cmd/cli: remove configure (BC) (#26633)

This commit is contained in:
andig 2026-01-12 09:27:19 +01:00 • committed by GitHub
parent ea18e19a7f
commit bd7a8ea6dd
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
15 changed files with 1 additions and 2285 deletions

View file

@ -1,64 +0,0 @@
package cmd
import (
"os"
"os/signal"
"syscall"
"github.com/evcc-io/evcc/cmd/configure"
"github.com/evcc-io/evcc/util"
"github.com/spf13/cobra"
)
// configureCmd represents the configure command
var configureCmd = &cobra.Command{
Use: "configure",
Short: "Create configuration (evcc.yaml)",
Run: runConfigure,
}
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")
configureCmd.Flags().String("category", "", "Pre-select device category for advanced configuration (implies advanced)")
}
func runConfigure(cmd *cobra.Command, args []string) {
impl := &configure.CmdConfigure{}
lang, err := cmd.Flags().GetString("lang")
if err != nil {
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)
}
category, err := cmd.Flags().GetString("category")
if err != nil {
panic(err)
}
util.LogLevel(viper.GetString("log"), nil)
// catch signals
go func() {
signalC := make(chan os.Signal, 1)
signal.Notify(signalC, os.Interrupt, syscall.SIGTERM)
<-signalC // wait for signal
os.Exit(1)
}()
impl.Run(log, lang, advanced, expand, category)
}

View file

@ -1,128 +0,0 @@
package configure
import (
"bytes"
_ "embed"
"text/template"
"github.com/Masterminds/sprig/v3"
"github.com/evcc-io/evcc/util/templates"
)
type device struct {
Name string
Title string
Yaml string
ChargerHasMeter bool // only used with chargers to detect if we need to ask for a charge meter
}
type loadpoint struct {
Title string // TODO Perspektivisch können wir was aus core wiederverwenden, für später
Charger string
ChargeMeter string
Vehicle string
Mode string
MinCurrent int
MaxCurrent int
Phases int
}
type globalConfig struct {
Meters []device
Chargers []device
Vehicles []device
Loadpoints []loadpoint
Site struct { // TODO Perspektivisch können wir was aus core wiederverwenden, für später
Title string
Grid string
PVs []string
Batteries []string
}
Hems string
EEBUS string
MQTT string
SponsorToken string
Plant string
Telemetry bool
}
type Configure struct {
config globalConfig
}
// AddDevice adds a device reference of a specific category to the configuration
// e.g. a PV meter to site.PVs
func (c *Configure) AddDevice(d device, category DeviceCategory) {
switch DeviceCategories[category].class {
case templates.Charger:
c.config.Chargers = append(c.config.Chargers, d)
case templates.Meter:
c.config.Meters = append(c.config.Meters, d)
switch DeviceCategories[category].categoryFilter {
case DeviceCategoryGridMeter:
c.config.Site.Grid = d.Name
case DeviceCategoryPVMeter:
c.config.Site.PVs = append(c.config.Site.PVs, d.Name)
case DeviceCategoryBatteryMeter:
c.config.Site.Batteries = append(c.config.Site.Batteries, d.Name)
}
case templates.Vehicle:
c.config.Vehicles = append(c.config.Vehicles, d)
default:
panic("invalid class for category: " + category)
}
}
// DevicesOfClass returns all configured devices of a given DeviceClass
func (c *Configure) DevicesOfClass(class templates.Class) []device {
switch class {
case templates.Charger:
return c.config.Chargers
case templates.Meter:
return c.config.Meters
case templates.Vehicle:
return c.config.Vehicles
default:
panic("invalid class: " + class.String())
}
}
// AddLoadpoint adds a loadpoint to the configuration
func (c *Configure) AddLoadpoint(l loadpoint) {
c.config.Loadpoints = append(c.config.Loadpoints, l)
}
// MetersOfCategory returns the number of configured meters of a given DeviceCategory
func (c *Configure) MetersOfCategory(category DeviceCategory) int {
switch category {
case DeviceCategoryGridMeter:
if c.config.Site.Grid != "" {
return 1
}
case DeviceCategoryPVMeter:
return len(c.config.Site.PVs)
case DeviceCategoryBatteryMeter:
return len(c.config.Site.Batteries)
}
return 0
}
//go:embed configure.tpl
var configTmpl string
// RenderConfiguration creates a yaml configuration
func (c *Configure) RenderConfiguration() ([]byte, error) {
tmpl, err := template.New("yaml").Funcs(sprig.FuncMap()).Parse(configTmpl)
if err != nil {
panic(err)
}
out := new(bytes.Buffer)
err = tmpl.Execute(out, c.config)
return bytes.TrimSpace(out.Bytes()), err
}

View file

@ -1,87 +0,0 @@
# open evcc at http://evcc.local:7070
network:
schema: http
host: evcc # announces the hostname on mDNS
port: 7070
log: debug
levels:
cache: error
# unique installation id
plant: {{ .Plant }}
interval: 30s # control cycle interval
{{- if .SponsorToken }}
sponsortoken: {{ .SponsorToken }}
# sponsors can set telemetry: true to enable anonymous data aggregation
# see https://github.com/evcc-io/evcc/discussions/4554
telemetry: {{ .Telemetry }}
{{- end}}
{{- if .Meters }}
meters:
{{- range .Meters }}
- {{ .Yaml | indent 2 | trim }}
{{- end }}
{{- end }}
{{- if .Chargers }}
chargers:
{{- range .Chargers }}
- {{ .Yaml | indent 2 | trim }}
{{- end }}
{{- end }}
{{- if .Vehicles }}
vehicles:
{{- range .Vehicles }}
- {{ .Yaml | indent 2 | trim }}
{{- end }}
{{- end }}
{{- if .Chargers }}
loadpoints:
{{- range .Loadpoints }}
- title: {{ .Title }}
charger: {{ .Charger }}
{{- if .ChargeMeter }}
meter: {{ .ChargeMeter }}
{{- end }}
{{- if .Vehicle }}
vehicle: {{ .Vehicle }}
{{- end }}
mode: {{ .Mode }}
{{- end }}
{{- end }}
site:
title: {{ .Site.Title }}
meters:
{{- if .Site.Grid }}
grid: {{ .Site.Grid }}
{{- end }}
{{- if .Site.PVs }}
pv:
{{- range .Site.PVs }}
- {{ . }}
{{- end }}
{{- end }}
{{- if .Site.Batteries }}
battery:
{{- range .Site.Batteries }}
- {{ . }}
{{- end }}
{{- end }}
{{- if .Hems }}
hems:
{{ .Hems | indent 2 }}
{{- end }}
{{- if .EEBUS }}
eebus:
{{ .EEBUS | indent 2 }}
{{- end }}

View file

@ -1,159 +0,0 @@
package configure
import (
"context"
"errors"
"time"
"github.com/evcc-io/evcc/api"
"github.com/evcc-io/evcc/charger"
"github.com/evcc-io/evcc/meter"
"github.com/evcc-io/evcc/util/templates"
"github.com/evcc-io/evcc/vehicle"
"go.yaml.in/yaml/v4"
)
type DeviceTestResult string
const (
DeviceTestResultValid DeviceTestResult = "Valid"
DeviceTestResultValidMissingMeter DeviceTestResult = "Valid_MissingMeter"
DeviceTestResultInvalid DeviceTestResult = "Invalid"
)
type DeviceTest struct {
DeviceCategory DeviceCategory
Template templates.Template
ConfigValues map[string]any
}
// Test returns:
// - DeviceTestResult: Valid, Valid_MissingMeter, Invalid
// - error: != nil if the device is invalid and can not be configured with the provided settings
func (d *DeviceTest) Test() (DeviceTestResult, error) {
v, err := d.configure()
if err != nil {
return DeviceTestResultInvalid, err
}
switch DeviceCategories[d.DeviceCategory].class {
case templates.Charger:
return d.testCharger(v)
case templates.Meter:
return d.testMeter(d.DeviceCategory, v)
case templates.Vehicle:
return d.testVehicle(v)
default:
panic("invalid class for category: " + d.DeviceCategory)
}
}
// configure creates a configured device from a template so we can test it
func (d *DeviceTest) configure() (any, error) {
b, _, err := d.Template.RenderResult(templates.RenderModeInstance, d.ConfigValues)
if err != nil {
return nil, err
}
var instance struct {
Type string
Other map[string]any `yaml:",inline"`
}
if err := yaml.Unmarshal(b, &instance); err != nil {
return nil, err
}
var v any
ctx := context.TODO()
switch DeviceCategories[d.DeviceCategory].class {
case templates.Meter:
v, err = meter.NewFromConfig(ctx, instance.Type, instance.Other)
case templates.Charger:
v, err = charger.NewFromConfig(ctx, instance.Type, instance.Other)
case templates.Vehicle:
v, err = vehicle.NewFromConfig(ctx, instance.Type, instance.Other)
}
return v, err
}
// testCharger tests a charger device
func (d *DeviceTest) testCharger(v any) (DeviceTestResult, error) {
c, ok := v.(api.Charger)
if !ok {
return DeviceTestResultInvalid, errors.New("selected device is not a wallbox")
}
if _, err := c.Status(); err != nil {
return DeviceTestResultInvalid, err
}
m, ok := v.(api.Meter)
if !ok {
return DeviceTestResultValidMissingMeter, nil
}
if _, err := m.CurrentPower(); err != nil {
return DeviceTestResultInvalid, err
}
return DeviceTestResultValid, nil
}
// testMeter tests a meter device
func (d *DeviceTest) testMeter(deviceCategory DeviceCategory, v any) (DeviceTestResult, error) {
m, ok := v.(api.Meter)
if !ok {
return DeviceTestResultInvalid, errors.New("selected device is not a meter")
}
power, err := m.CurrentPower()
if err != nil {
return DeviceTestResultInvalid, err
}
// check if the grid meter reports power 0, which should be impossible
// happens with Kostal Piko charger that do not have a grid meter attached
// but we can't determine this
if power == 0 && deviceCategory == DeviceCategoryGridMeter {
return DeviceTestResultInvalid, errors.New("grid meter reports power 0")
}
if deviceCategory == DeviceCategoryBatteryMeter {
b, ok := v.(api.Battery)
if !ok {
return DeviceTestResultInvalid, errors.New("selected device is not a battery meter")
}
_, err := b.Soc()
for err != nil && errors.Is(err, api.ErrMustRetry) {
time.Sleep(3 * time.Second)
_, err = b.Soc()
}
if err != nil {
return DeviceTestResultInvalid, err
}
}
return DeviceTestResultValid, nil
}
// testVehicle tests a vehicle device
func (d *DeviceTest) testVehicle(v any) (DeviceTestResult, error) {
vv, ok := v.(api.Vehicle)
if !ok {
return DeviceTestResultInvalid, errors.New("selected device is not a vehicle")
}
if _, err := vv.Soc(); err != nil {
return DeviceTestResultInvalid, err
}
return DeviceTestResultValid, nil
}

View file

@ -1,53 +0,0 @@
package configure
import (
"fmt"
"github.com/evcc-io/evcc/cmd/shutdown"
"github.com/evcc-io/evcc/server/eebus"
"github.com/evcc-io/evcc/util"
)
// configureEEBus setup EEBus
func (c *CmdConfigure) configureEEBus(other map[string]any) error {
conf := eebus.Config{
URI: ":4712",
}
if err := util.DecodeOther(other, &conf); err != nil {
return err
}
srv, err := eebus.NewServer(conf)
if err == nil {
eebus.Instance = srv
go eebus.Instance.Run()
shutdown.Register(eebus.Instance.Shutdown)
}
return err
}
// eebusCertificate creates EEBUS certificate and returns private/public key
func (c *CmdConfigure) eebusCertificate() (map[string]any, error) {
var eebusConfig map[string]any
cert, err := eebus.CreateCertificate()
if err != nil {
return eebusConfig, fmt.Errorf("%s", c.localizedString("Error_EEBUS_Certificate_Create"))
}
pubKey, privKey, err := eebus.GetX509KeyPair(cert)
if err != nil {
return eebusConfig, fmt.Errorf("%s", c.localizedString("Error_EEBUS_Certificate_Use"))
}
eebusConfig = map[string]any{
"certificate": map[string]string{
"public": pubKey,
"private": privKey,
},
}
return eebusConfig, nil
}

View file

@ -1,232 +0,0 @@
package configure
import (
"errors"
"fmt"
"slices"
"github.com/evcc-io/evcc/util/templates"
)
// configureDeviceGuidedSetup lets the user choose a device that is set to support guided setup
// these are typically devices that
// - contain multiple usages but have the same parameters like host, port, etc.
// - devices that typically are installed with additional specific devices (e.g. SMA Home Manager with SMA Inverters)
func (c *CmdConfigure) configureDeviceGuidedSetup() {
var err error
var values map[string]any
var deviceCategory DeviceCategory
var supportedDeviceCategories []DeviceCategory
var templateItem templates.Template
deviceItem := device{}
for {
fmt.Println()
templateItem, err = c.processDeviceSelection(DeviceCategoryGuidedSetup)
if err != nil {
return
}
usageChoices := c.paramChoiceValues(templateItem.Params, templates.ParamUsage)
if len(usageChoices) == 0 {
panic("ERROR: Device template is missing valid usages!")
}
if len(usageChoices) == 0 {
usageChoices = []string{string(DeviceCategoryGridMeter), string(DeviceCategoryPVMeter), string(DeviceCategoryBatteryMeter)}
}
supportedDeviceCategories = []DeviceCategory{}
for _, usage := range usageChoices {
switch usage {
case string(DeviceCategoryGridMeter):
supportedDeviceCategories = append(supportedDeviceCategories, DeviceCategoryGridMeter)
case string(DeviceCategoryPVMeter):
supportedDeviceCategories = append(supportedDeviceCategories, DeviceCategoryPVMeter)
case string(DeviceCategoryBatteryMeter):
supportedDeviceCategories = append(supportedDeviceCategories, DeviceCategoryBatteryMeter)
}
}
// we only ask for the configuration for the first usage
deviceCategory = supportedDeviceCategories[0]
values = c.processConfig(&templateItem, deviceCategory)
deviceItem, err = c.processDeviceValues(values, templateItem, deviceItem, deviceCategory)
if err != nil {
if err != c.errDeviceNotValid {
fmt.Println()
fmt.Println(err)
}
fmt.Println()
if !c.askConfigFailureNextStep() {
return
}
continue
}
break
}
c.configuration.AddDevice(deviceItem, deviceCategory)
c.processDeviceCapabilities(templateItem.Capabilities)
if len(supportedDeviceCategories) > 1 {
for _, additionalCategory := range supportedDeviceCategories[1:] {
values[templates.ParamUsage] = additionalCategory.String()
deviceItem, err := c.processDeviceValues(values, templateItem, deviceItem, additionalCategory)
if err != nil {
continue
}
c.configuration.AddDevice(deviceItem, additionalCategory)
}
}
fmt.Println()
fmt.Println(templateItem.Title() + " " + c.localizedString("Device_Added"))
c.configureLinkedTypes(templateItem)
}
// configureLinkedTypes lets the user configure devices that are marked as being linked to a guided device
// e.g. SMA Inverters, Energy Meter with SMA Home Manager
func (c *CmdConfigure) configureLinkedTypes(templateItem templates.Template) {
added := make([]string, 0)
for _, linkedTemplate := range templateItem.Linked {
// don't process this linked template if a referenced exclude template was added
if slices.Contains(added, linkedTemplate.ExcludeTemplate) {
continue
}
linkedTemplateItem, err := templates.ByName(templates.Meter, linkedTemplate.Template)
if err != nil {
fmt.Println("Error: " + err.Error())
return
}
if len(linkedTemplateItem.Params) == 0 || linkedTemplate.Usage == "" {
break
}
linkedTemplateItem.SetCombinedTitle(c.lang)
category := DeviceCategory(linkedTemplate.Usage)
localizeMap := localizeMap{
"Linked": linkedTemplateItem.Title(),
"Article": DeviceCategories[category].article,
"Additional": DeviceCategories[category].additional,
"Category": DeviceCategories[category].title,
}
fmt.Println()
if !c.askYesNo(c.localizedString("AddLinkedDeviceInCategory", localizeMap)) {
continue
}
for {
if c.configureLinkedTemplate(linkedTemplateItem, category) {
added = append(added, linkedTemplate.Template)
}
if !linkedTemplate.Multiple {
break
}
fmt.Println()
if !c.askYesNo(c.localizedString("AddAnotherLinkedDeviceInCategory", localizeMap)) {
break
}
}
}
}
// configureLinkedTemplate lets the user configure a device that is marked as being linked to a guided device
// returns true if a device was added
func (c *CmdConfigure) configureLinkedTemplate(templateItem templates.Template, category DeviceCategory) bool {
for {
deviceItem := device{}
values := c.processConfig(&templateItem, category)
deviceItem, err := c.processDeviceValues(values, templateItem, deviceItem, category)
if err != nil {
if !errors.Is(err, c.errDeviceNotValid) {
fmt.Println()
fmt.Println(err)
}
fmt.Println()
if c.askConfigFailureNextStep() {
continue
}
} else {
c.configuration.AddDevice(deviceItem, category)
c.processDeviceCapabilities(templateItem.Capabilities)
fmt.Println()
fmt.Println(templateItem.Title() + " " + c.localizedString("Device_Added"))
return true
}
break
}
return false
}
// configureDeviceCategory lets the user select and configure a device from a specific category
func (c *CmdConfigure) configureDeviceCategory(deviceCategory DeviceCategory) (device, []string, error) {
fmt.Println()
fmt.Printf("- %s %s\n", c.localizedString("Device_Configure"), DeviceCategories[deviceCategory].title)
device := device{
Name: DeviceCategories[deviceCategory].defaultName,
}
var deviceDescription string
var capabilities []string
// repeat until the device is added or the user chooses to continue without adding a device
for {
fmt.Println()
templateItem, err := c.processDeviceSelection(deviceCategory)
if err != nil {
return device, capabilities, c.errItemNotPresent
}
deviceDescription = templateItem.Title()
capabilities = templateItem.Capabilities
values := c.processConfig(&templateItem, deviceCategory)
device, err = c.processDeviceValues(values, templateItem, device, deviceCategory)
if err != nil {
if err != c.errDeviceNotValid {
fmt.Println()
fmt.Println(err)
}
// ask if the user wants to add the
fmt.Println()
if !c.askConfigFailureNextStep() {
return device, capabilities, err
}
continue
}
break
}
c.configuration.AddDevice(device, deviceCategory)
c.processDeviceCapabilities(capabilities)
var deviceTitle string
if device.Title != "" {
deviceTitle = " " + device.Title
}
fmt.Println()
fmt.Println(deviceDescription + deviceTitle + " " + c.localizedString("Device_Added"))
return device, capabilities, nil
}

View file

@ -1,502 +0,0 @@
package configure
import (
"fmt"
"slices"
"sort"
"strings"
"github.com/evcc-io/evcc/plugin/mqtt"
"github.com/evcc-io/evcc/util"
"github.com/evcc-io/evcc/util/sponsor"
"github.com/evcc-io/evcc/util/templates"
stripmd "github.com/writeas/go-strip-markdown/v2"
"go.yaml.in/yaml/v4"
)
// processDeviceSelection processes the user-selected device, checks
// if it's an actual device and makes sure the requirements are set
func (c *CmdConfigure) processDeviceSelection(deviceCategory DeviceCategory) (templates.Template, error) {
templateItem := c.selectItem(deviceCategory)
if templateItem.Title() == c.localizedString("ItemNotPresent") {
return templateItem, c.errItemNotPresent
}
if err := c.processDeviceRequirements(templateItem); err != nil {
return templateItem, err
}
return templateItem, nil
}
// processDeviceValues processes the user provided values, creates
// a device configuration and check if it is a valid device
func (c *CmdConfigure) processDeviceValues(values map[string]any, templateItem templates.Template, device device, deviceCategory DeviceCategory) (device, error) {
c.addedDeviceIndex++
device.Name = fmt.Sprintf("%s%d", DeviceCategories[deviceCategory].defaultName, c.addedDeviceIndex)
device.Title = templateItem.Title()
for item, value := range values {
if strings.ToLower(item) != "title" {
continue
}
if len(value.(string)) > 0 {
device.Title = value.(string)
}
}
var categoryWithUsage bool
fmt.Println()
switch deviceCategory {
case DeviceCategoryPVMeter, DeviceCategoryBatteryMeter, DeviceCategoryGridMeter:
categoryWithUsage = true
fmt.Println(c.localizedString("TestingDevice_TitleUsage", localizeMap{"Device": templateItem.Title(), "Usage": deviceCategory.String()}))
default:
fmt.Println(c.localizedString("TestingDevice_Title", localizeMap{"Device": templateItem.Title()}))
}
deviceTest := DeviceTest{
DeviceCategory: deviceCategory,
Template: templateItem,
ConfigValues: values,
}
testResult, err := deviceTest.Test()
if err != nil {
fmt.Println(" ", c.localizedString("Error", localizeMap{"Error": err}))
fmt.Println()
question := c.localizedString("TestingDevice_AddFailed", localizeMap{"Device": templateItem.Title()})
if categoryWithUsage {
question = c.localizedString("TestingDevice_AddFailedUsage", localizeMap{"Device": templateItem.Title(), "Usage": deviceCategory.String()})
}
if !c.askYesNo(question) {
c.addedDeviceIndex--
return device, c.errDeviceNotValid
}
} else if deviceCategory == DeviceCategoryCharger && testResult == DeviceTestResultValid {
device.ChargerHasMeter = true
}
templateItem.Params = append(templateItem.Params, templates.Param{Name: "name", Value: device.Name})
if !c.expandedMode {
b, err := templateItem.RenderProxyWithValues(values, c.lang)
if err != nil {
c.addedDeviceIndex--
return device, err
}
device.Yaml = string(b)
} else {
for _, p := range templateItem.Params {
if p.Name == "name" {
values["name"] = p.Value
templateItem.Render = fmt.Sprintf("name: {{ .name }}\n%s", templateItem.Render)
}
}
b, _, err := templateItem.RenderResult(templates.RenderModeInstance, values)
if err != nil {
c.addedDeviceIndex--
return device, err
}
device.Yaml = string(b)
}
return device, nil
}
func (c *CmdConfigure) processDeviceCapabilities(capabilitites []string) {
if slices.Contains(capabilitites, templates.CapabilitySMAHems) {
c.capabilitySMAHems = true
}
}
// processDeviceRequirements handles device requirements
func (c *CmdConfigure) processDeviceRequirements(templateItem templates.Template) error {
requirementDescription := stripmd.Strip(templateItem.Requirements.Description.String(c.lang))
if len(requirementDescription) > 0 {
fmt.Println()
fmt.Println("-------------------------------------------------")
fmt.Println(c.localizedString("Requirements_Title"))
fmt.Println(requirementDescription)
fmt.Println("-------------------------------------------------")
}
// check if sponsorship is required
if slices.Contains(templateItem.Requirements.EVCC, templates.RequirementSponsorship) && c.configuration.config.SponsorToken == "" {
if err := c.askSponsortoken(true, false); err != nil {
return err
}
}
// check if we need to setup an MQTT broker
if slices.Contains(templateItem.Requirements.EVCC, templates.RequirementMQTT) {
if c.configuration.config.MQTT == "" {
mqttConfig, err := c.configureMQTT(templateItem)
if err != nil {
return err
}
mqttYaml, err := yaml.Marshal(mqttConfig)
if err != nil {
return err
}
c.configuration.config.MQTT = string(mqttYaml)
}
}
// check if we need to setup an EEBUS HEMS
if slices.Contains(templateItem.Requirements.EVCC, templates.RequirementEEBUS) {
if c.configuration.config.EEBUS == "" {
fmt.Println()
fmt.Println("-- EEBUS -----------------------------------")
fmt.Println()
eebusConfig, err := c.eebusCertificate()
if err != nil {
return fmt.Errorf("%s: %s", c.localizedString("Requirements_EEBUS_Cert_Error"), err)
}
if err := c.configureEEBus(eebusConfig); err != nil {
return err
}
eebusYaml, err := yaml.Marshal(eebusConfig)
if err != nil {
return err
}
c.configuration.config.EEBUS = string(eebusYaml)
fmt.Println()
fmt.Println("--------------------------------------------")
}
fmt.Println()
fmt.Println(c.localizedString("Requirements_EEBUS_Pairing"))
fmt.Scanln()
}
return nil
}
func (c *CmdConfigure) askSponsortoken(required, feature bool) error {
fmt.Println("-- Sponsorship -----------------------------")
if required {
fmt.Println()
if feature {
fmt.Println(c.localizedString("Requirements_Sponsorship_Feature_Title"))
} else {
fmt.Println(c.localizedString("Requirements_Sponsorship_Title"))
}
} else {
fmt.Println()
fmt.Println(c.localizedString("Requirements_Sponsorship_Optional_Title"))
}
fmt.Println()
if !c.askYesNo(c.localizedString("Requirements_Sponsorship_Token")) {
fmt.Println()
fmt.Println("--------------------------------------------")
return c.errItemNotPresent
}
sponsortoken := c.askValue(question{
label: c.localizedString("Requirements_Sponsorship_Token_Input"),
mask: true,
required: true,
})
err := sponsor.ConfigureSponsorship(sponsortoken)
if err != nil {
question := c.localizedString("TestingDevice_AddFailed", localizeMap{"Device": "Sponsorship Token"})
if c.askYesNo(question) {
err = nil
}
}
if err == nil {
c.configuration.config.SponsorToken = sponsortoken
}
fmt.Println()
fmt.Println("--------------------------------------------")
return err
}
func (c *CmdConfigure) configureMQTT(_ templates.Template) (map[string]any, error) {
fmt.Println()
fmt.Println("-- MQTT Broker ----------------------------")
var err error
for {
fmt.Println()
_, paramHost := templates.ConfigDefaults.ParamByName("host")
_, paramPort := templates.ConfigDefaults.ParamByName("port")
_, paramUser := templates.ConfigDefaults.ParamByName("user")
_, paramPassword := templates.ConfigDefaults.ParamByName("password")
_, paramCaCert := templates.ConfigDefaults.ParamByName("caCert")
_, paramClientCert := templates.ConfigDefaults.ParamByName("clientCert")
_, paramClientKey := templates.ConfigDefaults.ParamByName("clientKey")
host := c.askParam(paramHost)
port := c.askParam(paramPort)
user := c.askParam(paramUser)
password := c.askParam(paramPassword)
caCert := c.askParam(paramCaCert)
clientCert := c.askParam(paramClientCert)
clientKey := c.askParam(paramClientKey)
fmt.Println()
fmt.Println("--------------------------------------------")
broker := fmt.Sprintf("%s:%s", host, port)
mqttConfig := map[string]any{
"broker": broker,
"user": user,
"password": password,
"caCert": caCert,
"clientCert": clientCert,
"clientKey": clientKey,
}
log := util.NewLogger("mqtt")
if mqtt.Instance, err = mqtt.RegisteredClient(log, broker, user, password, "", 1, false, caCert, clientCert, clientKey); err == nil {
return mqttConfig, nil
}
fmt.Println()
question := c.localizedString("TestingMQTTFailed")
if !c.askYesNo(question) {
return nil, fmt.Errorf("failed configuring mqtt: %w", err)
}
}
}
// fetchElements returns template items of a given class
func (c *CmdConfigure) fetchElements(deviceCategory DeviceCategory) []templates.Template {
var items []templates.Template
for _, tmpl := range templates.ByClass(DeviceCategories[deviceCategory].class) {
if len(tmpl.Params) == 0 {
continue
}
for _, t := range tmpl.Titles(c.lang) {
titleTmpl := templates.Template{
TemplateDefinition: tmpl.TemplateDefinition,
}
title := t
groupTitle := titleTmpl.GroupTitle(c.lang)
if groupTitle != "" {
title += " [" + groupTitle + "]"
}
titleTmpl.SetTitle(title)
if deviceCategory == DeviceCategoryGuidedSetup {
if tmpl.GuidedSetupEnabled() {
items = append(items, titleTmpl)
}
} else {
if len(DeviceCategories[deviceCategory].categoryFilter) == 0 ||
c.paramChoiceContains(tmpl.Params, templates.ParamUsage, DeviceCategories[deviceCategory].categoryFilter.String()) {
items = append(items, titleTmpl)
}
}
}
}
sort.Slice(items, func(i, j int) bool {
// sort generic templates to the bottom
if items[i].Group != "" && items[j].Group == "" {
return false
}
if items[i].Group == "" && items[j].Group != "" {
return true
}
if items[i].Group != items[j].Group {
return strings.ToLower(items[i].GroupTitle(c.lang)) < strings.ToLower(items[j].GroupTitle(c.lang))
}
return strings.ToLower(items[i].Title()) < strings.ToLower(items[j].Title())
})
return items
}
// paramChoiceContains is a helper function to check if a param choice contains a given value
func (c *CmdConfigure) paramChoiceContains(params []templates.Param, name, filter string) bool {
choices := c.paramChoiceValues(params, name)
return slices.Contains(choices, filter)
}
// paramChoiceValues provides a list of possible values for a param choice
func (c *CmdConfigure) paramChoiceValues(params []templates.Param, name string) []string {
choices := []string{}
for _, item := range params {
if item.Name != name {
continue
}
choices = append(choices, item.Choice...)
}
return choices
}
// processConfig processes an EVCC configuration item
// Returns a map with param name and values
func (c *CmdConfigure) processConfig(templateItem *templates.Template, deviceCategory DeviceCategory) map[string]any {
fmt.Println()
fmt.Println(c.localizedString("Config_Title"))
fmt.Println()
c.processModbusConfig(templateItem)
return c.processParams(templateItem, deviceCategory)
}
// process a list of params
func (c *CmdConfigure) processParams(templateItem *templates.Template, deviceCategory DeviceCategory) map[string]any {
usageFilter := DeviceCategories[deviceCategory].categoryFilter
additionalConfig := make(map[string]any)
for _, param := range templateItem.Params {
switch param.Name {
case templates.ParamModbus:
additionalConfig[param.Name] = param.Value
case templates.ParamUsage:
if usageFilter != "" {
additionalConfig[param.Name] = usageFilter.String()
}
default:
if param.IsAdvanced() && !c.advancedMode || param.IsDeprecated() {
continue
}
if usageFilter != "" && len(param.Usages) > 0 && !slices.Contains(param.Usages, string(usageFilter)) {
continue
}
switch param.Type {
case templates.TypeList:
values := c.processListInputConfig(param)
var nonEmptyValues []string
for _, value := range values {
if value != "" {
nonEmptyValues = append(nonEmptyValues, value)
}
}
additionalConfig[param.Name] = nonEmptyValues
default:
// TODO make processInputConfig aware of default values added by template
if value := c.processInputConfig(param); value != "" {
additionalConfig[param.Name] = value
}
}
}
}
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 user decides to stop
for {
newValue := c.processInputConfig(param)
values = append(values, newValue)
if newValue == "" {
break
}
if !c.askYesNo(" " + c.localizedString("Config_AddAnotherValue")) {
break
}
}
return values
}
// handle user input for a simple one value input
func (c *CmdConfigure) processInputConfig(param templates.Param) string {
label := param.Name
langLabel := param.Description.String(c.lang)
if langLabel != "" {
label = langLabel
}
if param.Unit != "" {
label = fmt.Sprintf("%s (%s)", label, param.Unit)
}
value := c.askValue(question{
label: label,
defaultValue: param.Default,
exampleValue: param.Example,
help: param.Help.ShortString(c.lang),
valueType: param.Type,
choice: param.Choice,
mask: param.IsMasked(),
required: param.IsRequired(),
})
return value
}
// processModbusConfig adds default values from the modbus Param to the template
// and handles user input for interface type selection
func (c *CmdConfigure) processModbusConfig(templateItem *templates.Template) {
var choices []string
var choiceTypes []string
modbusIndex, modbusParam := templateItem.ParamByName(templates.ParamModbus)
if modbusIndex == -1 {
return
}
config := templates.ConfigDefaults.Modbus
for _, choice := range modbusParam.Choice {
if config.Interfaces[choice] == nil {
continue
}
for _, itype := range config.Interfaces[choice] {
title := config.Types[itype].Description
choices = append(choices, title.String(c.lang))
choiceTypes = append(choiceTypes, itype)
}
}
if len(choices) == 0 {
return
}
// ask for modbus interface type
var index int
if len(choices) > 1 {
index, _ = c.askChoice(c.localizedString("Config_ModbusInterface"), choices)
}
values := make(map[string]any)
templateItem.Params[modbusIndex].Value = choiceTypes[index]
// add the interface type specific modbus params
templateItem.ModbusParams(choiceTypes[index], values)
// update the modbus default values
templateItem.ModbusValues(templates.RenderModeInstance, values)
}

View file

@ -1,113 +0,0 @@
Intro = "Die nächsten Schritte führen durch die Einrichtung einer Konfigurationsdatei für evcc.\nBeachte dass dieser Prozess nicht alle möglichen Szenarien berücksichtigen kann.\nDurch Drücken von CTRL-C kann der Prozess abgebrochen werden.\n\nACHTUNG: Diese Funktionalität hat experimentellen Status!\n D.h. es kann möglich sein, dass die hiermit erstellte Konfigurationsdatei\n in einem Update nicht mehr funktionieren könnte und neu erzeugt werden müsste.\n Wir freuen uns auf euer Feedback auf https://github.com/evcc-io/evcc/discussions/\n\nAuf geht's:"
Flow_Mode = "In welchem Modus soll die Konfiguration durchgeführt werden?"
Flow_Mode_Standard = "Standard Modus (So einfach und schnell wie möglich)"
Flow_Mode_Advanced = "Fortgeschrittener Modus (Detailliertere Fragen, erfordert jedoch technisches Know-How)"
Flow_Type = "Was möchtest du machen?"
Flow_Type_NewConfiguration = "Eine neue evcc Konfigurationsdatei erstellen"
Flow_Type_SingleDevice = "Ein einzelnes Gerät konfigurieren (muss manuell in eine Konfigurationsdatei eingetragen werden!)"
Flow_NewConfiguration_Setup = "- Hausinstallation einrichten"
Flow_NewConfiguration_Select = "Wähle eines der folgenden integrierten PV-Systeme aus, oder '{{ .ItemNotPresent }}' falls es kein integriertes PV-System vorhanden ist"
Flow_SingleDevice_Setup = "- Ein Gerät konfigurieren"
Flow_SingleDevice_Select = "Wähle eine der folgenden Gerätekategorien aus:"
Flow_SingleDevice_Config = "Die Konfiguration lautet:"
Flow_SMAHems_Setup = "- SMA HEMS konfigurieren"
Flow_SMAHems_Add = "Möchtest du die Wallboxen an den SMA Home Manager anbinden, damit diese z.B. für die Steuerung der Hausbatterie berücksichtigt werden können?"
ItemNotPresent = "Mein Gerät ist nicht in der Liste"
AddDeviceInCategory = "Möchtest du {{ .Article }} {{ .Category }} hinzufügen?"
AddAnotherDeviceInCategory = "Möchtest du noch {{ .Additional }} {{ .Category }} hinzufügen?"
AddLinkedDeviceInCategory = "Möchtest du ein '{{ .Linked }}' Gerät als {{ .Article }} {{ .Category }} hinzufügen?"
AddAnotherLinkedDeviceInCategory = "Möchtest du noch ein '{{ .Linked }}' Gerät als {{ .Article }} {{ .Category }} hinzufügen?"
Error = "Fehler: {{ .Error }}"
Error_ItemNotPresent = "Gerät nicht vorhanden"
Error_DeviceNotValid = "Das Gerät funktioniert nicht"
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"
Choose = "Wähle"
Category_ChargerTitle = "Wallbox"
Category_ChargerArticle = "eine"
Category_ChargerAdditional = "eine weitere"
Category_SystemTitle = "integriertes PV-System"
Category_SystemArticle = "ein"
Category_SystemAdditional = "ein weiteres"
Category_GridMeterTitle = "Netz-Stromzähler"
Category_GridMeterArticle = "einen"
Category_GridMeterAdditional = "einen weiteren"
Category_PVMeterTitle = "PV Wechselrichter (oder entsprechender Stromzähler)"
Category_PVMeterArticle = "einen"
Category_PVMeterAdditional = "einen weiteren"
Category_BatteryMeter = "Batterie Wechselrichter (oder entsprechender Stromzähler)"
Category_BatteryMeterArticle = "einen"
Category_BatteryMeterAdditional = "einen weiteren"
Category_ChargeMeterTitle = "Ladestromzähler"
Category_ChargeMeterArticle = "einen"
Category_ChargeMeterAdditional = "einen weiteren"
Category_VehicleTitle = "Fahrzeug"
Category_VehicleArticle = "ein"
Category_VehicleAdditional = "ein weiteres"
TestingDevice_Title = "Teste die Konfiguration von {{ .Device }} ..."
TestingDevice_TitleUsage = "Teste die {{ .Usage}} Konfiguration von {{ .Device }} ..."
TestingDevice_RepeatStep = "Möchtest du erneut ein Gerät aus der Liste auswählen und einrichten?"
TestingDevice_AddFailed = "Der Test von {{ .Device }} ist fehlgeschlagen. Soll es trotzdem in die Konfiguration aufgenommen werden?"
TestingDevice_AddFailedUsage = "Der Test der {{ .Usage }} Konfiguration von {{ .Device }} ist fehlgeschlagen. Soll {{ .Usage }} trotzdem in die Konfiguration aufgenommen werden?"
TestingMQTTFailed = "Der Test der MQTT Konfiguration ist fehlgeschlagen. Möchtest du die Konfiguration wiederholen?"
Requirements_Title = "Das Gerät hat die folgenden Voraussetzungen:"
Requirements_More = "Weitere Informationen:"
Requirements_Sponsorship_Title = "Dieses Gerät benötigt ein Sponsorship von evcc. Wie das funktioniert und was das ist, findest du hier: https://docs.evcc.io/docs/sponsorship"
Requirements_Sponsorship_Optional_Title = "Die Entwicklung von evcc kann über Sponsoring unterstützt werden. Wie das funktioniert und was das ist, findest du hier: https://docs.evcc.io/docs/sponsorship"
Requirements_Sponsorship_Feature_Title = "Dies Verwendung dieser Funktionalität benötigt ein Sponsorship von evcc. Wie das funktioniert und was das ist, findest du hier: https://docs.evcc.io/docs/sponsorship"
Requirements_Sponsorship_Token = "Bist du ein Sponsor?"
Requirements_Sponsorship_Token_Input = "Bitte gib das Sponsortoken ein"
Requirements_EEBUS_Cert_Error = "Fehler: Das EEBUS Zertifikat konnte nicht erstellt werden"
Requirements_EEBUS_Pairing = "Du hast eine Wallbox ausgewählt, welche über das EEBUS Protokoll angesprochen wird.\nDazu muss die Wallbox nun mit evcc verbunden werden. Dies geschieht üblicherweise auf der Webseite der Wallbox.\nDrücke die Enter-Taste, wenn der Prozess gestartet ist."
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 \n"
InputError = "Bei der Eingabe ist ein Fehler aufgetreten:"
Value_Choice = "Auswahl:"
Value_Help = "Hilfe:"
Value_Required = "erforderlich"
Value_Optional = "optional"
Value_Sample = "Beispiel"
ValueError_Invalid = "Ungültiger Wert"
ValueError_Used = "Dieser Wert wird bereits verwendet."
ValueError_Empty = "Der Wert darf nicht leer sein."
ValueError_Float = "Der Wert muss eine Zahl sein. Nachkommastellen mit . anstatt mit , trennen."
ValueError_Number = "Der Wert muss eine ganzzahlige Zahl sein."
ValueError_NumberLowerThanMin = "Der Wert muss größer oder gleich {{ .Min }} sein."
ValueError_NumberBiggerThanMax = "Der Wert muss kleiner oder gleich {{ .Max }} sein."
ValueError_Duration = "Der Wert muss eine Zeitdauer angeben. Zum Beispiel: 1s, 1m, 1h"
Device_Configure = "Konfiguration"
Device_Added = "wurde erfolgreich hinzugefügt."
Loadpoint_Setup = "- Ladepunkt(e) einrichten"
Loadpoint_Title = "Titel des Ladepunktes"
Loadpoint_AddAnother = "Möchtest du einen weiteren Ladepunkt hinzufügen?"
Loadpoint_DefaultTitle = "Garage"
Loadpoint_ResetOnDisconnect = "Soll beim Abstecken des Ladekabels von einem Fahrzeug, die Lade-Standardeinstellungen wieder hergestellt werden?"
Loadpoint_WallboxWOMeter = "Das System konnte nicht erkennen, ob ein Ladestromzähler in der Wallbox eingebaut ist. Möchtest du einen externen Ladestromzähler hinzufügen?"
Loadpoint_WallboxMaxPower = "Was ist die maximale Leistung, welche die Wallbox zur Verfügung stellen kann?"
Loadpoint_WallboxMinAmperage = "Was ist die MINIMALE Stromstärke, welche die Wallbox auf einer Phase zur Verfügung stellen kann?"
Loadpoint_WallboxMaxAmperage = "Was ist die MAXIMALE Stromstärke, welche die Wallbox auf einer Phase zur Verfügung stellen kann?"
Loadpoint_WallboxPhases = "Mit wievielen Phasen ist die Wallbox angeschlossen?"
Loadpoint_WallboxPower36kW = "3,6 kW"
Loadpoint_WallboxPower11kW = "11 kW"
Loadpoint_WallboxPower22kW = "22 kW"
Loadpoint_WallboxPowerOther = "Andere Leistung"
Loadpoint_VehicleDisableAutoDetection = "Möchtest du die automatische Fahrzeugerkennung deaktivieren und ein Fahrzeug fest zuweisen? (Automatische Erkennung funktioniert nicht mit Offline Fahrzeugen!)"
Loadpoint_VehicleSelection = "Welches Fahrzeug soll hier fest zugewiesen werden?"
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"
Site_DefaultTitle = "Mein Zuhause"

View file

@ -1,113 +0,0 @@
Intro = "The next steps will guide you through the creation of a configuration file for evcc.\nPlease note that this process cannot cover all possible scenarios.\nYou can cancel the process by pressing CTRL-C anytime.\n\nNOTE: This functionality is in an experimental state!\n This means that the configuration file created may not work\n in a future update of evcc and would need to be recreated.\n We are looking forward to your feedback at https://github.com/evcc-io/evcc/discussions/\n\nLet's go:"
Flow_Mode = "In which mode should this process guide you through the configuration?"
Flow_Mode_Standard = "Standard mode (As simple and quick as possible)"
Flow_Mode_Advanced = "Advanced mode (Ask more details, requires technical know-how)"
Flow_Type = "What do you want to do?"
Flow_Type_NewConfiguration = "Create a new evcc configuration file"
Flow_Type_SingleDevice = "Configure a single device (has to be added manually to a configuration file!)"
Flow_NewConfiguration_Setup = "- Setup meters (house installation)"
Flow_NewConfiguration_Select = "Choose one of the following integrated PV systems, or '{{ .ItemNotPresent }}' if you have none of them"
Flow_SingleDevice_Setup = "- Setup a device"
Flow_SingleDevice_Select = "Choose one of the following device categories"
Flow_SingleDevice_Config = "The configuration:"
Flow_SMAHems_Setup = "- Setup SMA HEMS"
Flow_SMAHems_Add = "Do you want to connect your chargers to SMA Home Manager, so that it can consider them e.g. for controlling the in-house battery?"
ItemNotPresent = "My device is not in this list"
AddDeviceInCategory = "Do you want to add {{ .Article }} {{ .Category }}?"
AddAnotherDeviceInCategory = "Do you want to add {{ .Additional }} {{ .Category }}?"
AddLinkedDeviceInCategory = "Do you want to add a '{{ .Linked }}' device as {{ .Article }} {{ .Category }}?"
AddAnotherLinkedDeviceInCategory = "Do you want to add another '{{ .Linked }}' device as {{ .Article }} {{ .Category }}?"
Error = "Error: {{ .Error }}"
Error_ItemNotPresent = "Device not present"
Error_DeviceNotValid = "Device does not work"
Error_EEBUS_Certificate_Create = "Could not create the EEBUS certificate"
Error_EEBUS_Certificate_Use = "Could not process the 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 }}"
Choose = "Choose"
Category_ChargerTitle = "charger"
Category_ChargerArticle = "a"
Category_ChargerAdditional = "another"
Category_SystemTitle = "integrated PV system"
Category_SystemArticle = "an"
Category_SystemAdditional = "another"
Category_GridMeterTitle = "grid meter"
Category_GridMeterArticle = "a"
Category_GridMeterAdditional = "another"
Category_PVMeterTitle = "PV inverter (or corresponding meter)"
Category_PVMeterArticle = "a"
Category_PVMeterAdditional = "another"
Category_BatteryMeter = "battery inverter (or corresponding meter)"
Category_BatteryMeterArticle = "a"
Category_BatteryMeterAdditional = "another"
Category_ChargeMeterTitle = "charge meter"
Category_ChargeMeterArticle = "a"
Category_ChargeMeterAdditional = "another"
Category_VehicleTitle = "vehicle"
Category_VehicleArticle = "a"
Category_VehicleAdditional = "another"
TestingDevice_Title = "Testing the configuration of {{ .Device }} ..."
TestingDevice_TitleUsage = "Testing the {{ .Usage }} configuration of {{ .Device }} ..."
TestingDevice_RepeatStep = "Do you want to repeat choosing a device and configuring it?"
TestingDevice_AddFailed = "Testing {{ .Device }} failed. Do you want to add it anyway?"
TestingDevice_AddFailedUsage = "Testing of the {{ .Usage }} configuration of {{ .Device }} failed. Do you want to add {{ .Usage }} anyway?"
TestingMQTTFailed = "Testing the MQTT configuration failed. Do you want to repeat its configuration?"
Requirements_Title = "The device has the following requirements:"
Requirements_More = "Additional information:"
Requirements_Sponsorship_Title = "This device requires evcc sponsorship. Check the following link for what this is and how it works: https://docs.evcc.io/en/docs/sponsorship"
Requirements_Sponsorship_Optional_Title = "The development of evcc can be supported via sponsoring. Check the following link for what this is and how it works: https://docs.evcc.io/en/docs/sponsorship"
Requirements_Sponsorship_Feature_Title = "To use this feature evcc sponsorship is required. Check the following link for what this is and how it works: https://docs.evcc.io/en/docs/sponsorship"
Requirements_Sponsorship_Token = "Are you already a sponsor?"
Requirements_Sponsorship_Token_Input = "Please enter the sponsorship token"
Requirements_EEBUS_Cert_Error = "Error: The EEBUS certificate couldn't be created"
Requirements_EEBUS_Pairing = "You selected a charger which will be accessed via the EEBUS protocol.\nFor that, the charger must be connected to evcc. This can usually be done in the web interface of the charger.\nPlease press the enter key once you have started the process."
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, please try the manual configuration. You can find more details about that on our website: https://docs.evcc.io/en/ \n"
InputError = "An input error occurred:"
Value_Choice = "Choose:"
Value_Help = "Help:"
Value_Required = "required"
Value_Optional = "optional"
Value_Sample = "Example"
ValueError_Invalid = "This value is invalid"
ValueError_Used = "This value is already in use."
ValueError_Empty = "The value may not be empty."
ValueError_Float = "The value must be a number."
ValueError_Number = "The value must be an integer."
ValueError_NumberLowerThanMin = "The value must be bigger or equal to {{ .Min }}."
ValueError_NumberBiggerThanMax = "The value must be smaller or equal to {{ .Max }}."
ValueError_Duration = "The value must include a duration. For example: 1s, 1m, 1h"
Device_Configure = "Configuration"
Device_Added = "was successfully added."
Loadpoint_Setup = "- Setup loadpoint(s), i.e. group of colocated chargers"
Loadpoint_Title = "Loadpoint title"
Loadpoint_AddAnother = "Do you want to add another loadpoint?"
Loadpoint_DefaultTitle = "Garage"
Loadpoint_ResetOnDisconnect = "Will disconnecting the charging cable from the vehicle reset the charging settings to the defaults?"
Loadpoint_WallboxWOMeter = "The system could not detect if the charger provides power data. Do you want to add an external meter instead?"
Loadpoint_WallboxMaxPower = "What is the maximum power your charger can provide?"
Loadpoint_WallboxMinAmperage = "What is the MINIMUM amperage your charger can provide on a single phase?"
Loadpoint_WallboxMaxAmperage = "What is the MAXIMUM amperage your charger can provide on a single phase?"
Loadpoint_WallboxPhases = "How many phases are connected to the charger?"
Loadpoint_WallboxPower36kW = "3.6 kW"
Loadpoint_WallboxPower11kW = "11 kW"
Loadpoint_WallboxPower22kW = "22 kW"
Loadpoint_WallboxPowerOther = "Other option"
Loadpoint_VehicleDisableAutoDetection = "Do you want to disable the automatic vehicle detection and assign a fixed vehicle? (Automatic detection does not work with offline vehicles!)"
Loadpoint_VehicleSelection = "Which vehicle should be assigned here?"
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"
Site_DefaultTitle = "My home"

View file

@ -1,448 +0,0 @@
package configure
import (
"bufio"
_ "embed"
"errors"
"fmt"
"maps"
"os"
"slices"
"strconv"
"strings"
"github.com/BurntSushi/toml"
"github.com/cloudfoundry/jibber_jabber"
"github.com/evcc-io/evcc/hems/shm"
"github.com/evcc-io/evcc/util"
"github.com/evcc-io/evcc/util/machine"
"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
capabilitySMAHems bool
}
// Run starts the interactive configuration
func (c *CmdConfigure) Run(log *util.Logger, flagLang string, advancedMode, expandedMode bool, category string) {
c.log = log
c.advancedMode = advancedMode
c.expandedMode = expandedMode
c.log.INFO.Printf("evcc %s", util.FormattedVersion())
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()
// Assign random plant id. Don't use actual machine-id as file might
// be copied around to a different machine.
c.configuration.config.Plant = machine.RandomID()
if err = machine.CustomID(c.configuration.config.Plant); err != nil {
panic(err)
}
fmt.Println()
fmt.Println(c.localizedString("Intro"))
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"), []string{
c.localizedString("Flow_Mode_Standard"),
c.localizedString("Flow_Mode_Advanced"),
})
if flowIndex == 1 {
c.advancedMode = true
}
}
if !c.advancedMode && category == "" {
c.flowNewConfigFile()
return
}
if category != "" {
for cat := range DeviceCategories {
if cat == DeviceCategory(category) {
c.flowSingleDevice(DeviceCategory(category))
return
}
}
log.FATAL.Fatalln("invalid category:", category, "have:", slices.Collect(maps.Keys(DeviceCategories)))
}
fmt.Println()
flowIndex, _ := c.askChoice(c.localizedString("Flow_Type"), []string{
c.localizedString("Flow_Type_NewConfiguration"),
c.localizedString("Flow_Type_SingleDevice"),
})
switch flowIndex {
case 0:
c.flowNewConfigFile()
case 1:
c.flowSingleDevice("")
}
}
// configureSingleDevice implements the flow for getting a single device configuration
func (c *CmdConfigure) flowSingleDevice(category DeviceCategory) {
fmt.Println()
fmt.Println(c.localizedString("Flow_SingleDevice_Setup"))
fmt.Println()
fmt.Println(c.localizedString("Flow_SingleDevice_Select"))
// 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,
}
if category == "" {
fmt.Println()
_, categoryTitle := c.askChoice(c.localizedString("Flow_SingleDevice_Select"), categoryChoices)
for item, data := range DeviceCategories {
if data.title == categoryTitle {
category = item
break
}
}
}
devices := c.configureDevices(category, 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"))
fmt.Println()
fmt.Println(c.localizedString("Flow_NewConfiguration_Select", localizeMap{"ItemNotPresent": c.localizedString("ItemNotPresent")}))
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()
// check if SMA HEMS is available and ask the user if it should be added
if c.capabilitySMAHems {
c.configureSMAHems()
}
if c.advancedMode && c.configuration.config.SponsorToken == "" {
_ = c.askSponsortoken(false, false)
}
yaml, err := c.configuration.RenderConfiguration()
if err != nil {
c.log.FATAL.Fatal(err)
}
fmt.Println()
filename := DefaultConfigFilename
for {
file, err := os.OpenFile(filename, os.O_WRONLY, 0o666)
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"),
exampleValue: "evcc_neu.yaml",
required: true,
})
}
err = os.WriteFile(filename, yaml, 0o755)
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 specific 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 {
device, capabilities, err := c.configureDeviceCategory(deviceCategory)
if err != nil {
break
}
devices = append(devices, device)
c.processDeviceCapabilities(capabilities)
if !askMultiple {
break
}
fmt.Println()
if !c.askYesNo(c.localizedString("AddAnotherDeviceInCategory", localizeMap)) {
break
}
}
return devices
}
// configureSMAHems asks the user if he wants to add the SMA HEMS
func (c *CmdConfigure) configureSMAHems() {
// check if the system provides a machine-id
if _, err := shm.UniqueDeviceID(); err != nil {
return
}
fmt.Println()
fmt.Println(c.localizedString("Flow_SMAHems_Setup"))
fmt.Println()
if !c.askYesNo(c.localizedString("Flow_SMAHems_Add")) {
return
}
// check if we need to setup a HEMS
c.configuration.config.Hems = "type: sma\nAllowControl: false\n"
}
// configureLoadpoints asks loadpoint specific questions
func (c *CmdConfigure) configureLoadpoints() {
fmt.Println()
fmt.Println(c.localizedString("Loadpoint_Setup"))
for {
loadpointTitle := c.askValue(question{
label: c.localizedString("Loadpoint_Title"),
defaultValue: c.localizedString("Loadpoint_DefaultTitle"),
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")) {
chargeMeter, _, err := c.configureDeviceCategory(DeviceCategoryChargeMeter)
if err == nil {
loadpoint.ChargeMeter = chargeMeter.Name
chargerHasMeter = true
}
}
}
vehicles := c.configuration.DevicesOfClass(templates.Vehicle)
if len(vehicles) > 0 {
fmt.Println()
if c.askYesNo(c.localizedString("Loadpoint_VehicleDisableAutoDetection")) {
if len(vehicles) == 1 {
loadpoint.Vehicle = vehicles[0].Name
} else {
fmt.Println()
var vehicleTitles []string
for _, vehicle := range vehicles {
vehicleTitles = append(vehicleTitles, vehicle.Title)
}
vehicleIndex, _ := c.askChoice(c.localizedString("Loadpoint_VehicleSelection"), vehicleTitles)
loadpoint.Vehicle = vehicles[vehicleIndex].Name
}
}
}
var minValue int = 6
if slices.Contains(capabilities, templates.CapabilityISO151182) {
minValue = 2
}
if c.advancedMode {
fmt.Println()
minAmperage := c.askValue(question{
label: c.localizedString("Loadpoint_WallboxMinAmperage"),
valueType: templates.TypeInt,
minNumberValue: int64(minValue),
maxNumberValue: 32,
required: true,
})
loadpoint.MinCurrent, _ = strconv.Atoi(minAmperage)
maxAmperage := c.askValue(question{
label: c.localizedString("Loadpoint_WallboxMaxAmperage"),
valueType: templates.TypeInt,
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"), phaseChoices)
loadpoint.Phases = phaseIndex + 1
}
} else {
powerChoices := []string{
c.localizedString("Loadpoint_WallboxPower36kW"),
c.localizedString("Loadpoint_WallboxPower11kW"),
c.localizedString("Loadpoint_WallboxPower22kW"),
c.localizedString("Loadpoint_WallboxPowerOther"),
}
fmt.Println()
powerIndex, _ := c.askChoice(c.localizedString("Loadpoint_WallboxMaxPower"), 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"),
valueType: templates.TypeInt,
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"), phaseChoices)
loadpoint.Phases = phaseIndex + 1
}
}
}
fmt.Println()
loadpoint.Mode = c.askValue(question{valueType: templates.TypeChargeModes, excludeNone: true})
c.configuration.AddLoadpoint(loadpoint)
fmt.Println()
if !c.askYesNo(c.localizedString("Loadpoint_AddAnother")) {
break
}
}
}
// configureSite asks site specific questions
func (c *CmdConfigure) configureSite() {
fmt.Println()
fmt.Println(c.localizedString("Site_Setup"))
siteTitle := c.askValue(question{
label: c.localizedString("Site_Title"),
defaultValue: c.localizedString("Site_DefaultTitle"),
required: true,
})
c.configuration.config.Site.Title = siteTitle
}

View file

@ -1,261 +0,0 @@
package configure
import (
"errors"
"fmt"
"os"
"slices"
"strconv"
"strings"
"time"
"github.com/AlecAivazis/survey/v2"
"github.com/AlecAivazis/survey/v2/terminal"
"github.com/evcc-io/evcc/api"
"github.com/evcc-io/evcc/util/templates"
stripmd "github.com/writeas/go-strip-markdown/v2"
)
// surveyAskOne asks the user for input
func (c *CmdConfigure) surveyAskOne(p survey.Prompt, response any, opts ...survey.AskOpt) error {
opts = append(opts, survey.WithIcons(func(icons *survey.IconSet) {
icons.Question.Text = ""
}))
err := survey.AskOne(p, response, opts...)
if err != nil {
if err == terminal.InterruptErr {
fmt.Println(c.localizedString("Cancel"))
os.Exit(0)
}
fmt.Printf("%s %s\n", c.localizedString("InputError"), err)
}
return err
}
// askConfigFailureNextStep asks the user if he/she wants to select another device because the current does not work, or continue
func (c *CmdConfigure) askConfigFailureNextStep() bool {
fmt.Println()
return c.askYesNo(c.localizedString("TestingDevice_RepeatStep"))
}
// select item from list
func (c *CmdConfigure) askSelection(message string, items []string) (string, int, error) {
prompt := &survey.Select{
Message: message,
Options: items,
}
var selection string
err := c.surveyAskOne(prompt, &selection)
return selection, slices.Index(items, selection), err
}
// selectItem selects item from list
func (c *CmdConfigure) selectItem(deviceCategory DeviceCategory) templates.Template {
var emptyItem templates.Template
emptyItem.SetTitle(c.localizedString("ItemNotPresent"))
elements := c.fetchElements(deviceCategory)
elements = append(elements, emptyItem)
var items []string
for _, item := range elements {
if item.Title() != "" {
items = append(items, item.Title())
}
}
text := fmt.Sprintf("%s %s %s:", c.localizedString("Choose"), DeviceCategories[deviceCategory].article, DeviceCategories[deviceCategory].title)
_, selected, err := c.askSelection(text, items)
if err != nil {
c.log.FATAL.Fatal(err)
}
return elements[selected]
}
// askChoice selects item from list
func (c *CmdConfigure) askChoice(label string, choices []string) (int, string) {
selection, index, err := c.askSelection(label, choices)
if err != nil {
c.log.FATAL.Fatal(err)
}
return index, selection
}
// askYesNo asks yes/no question, return true if yes is selected
func (c *CmdConfigure) askYesNo(label string) bool {
prompt := &survey.Confirm{
Message: label,
}
var confirmation bool
if err := c.surveyAskOne(prompt, &confirmation); err != nil {
c.log.FATAL.Fatal(err)
}
return confirmation
}
type question struct {
label, help string
defaultValue, exampleValue string
invalidValues []string
choice []string
valueType templates.ParamType
minNumberValue, maxNumberValue int64
mask, required bool
excludeNone 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"), c.localizedString("Config_Yes")}
values := []string{"false", "true"}
index, _ := c.askChoice(label, choices)
return values[index]
}
func (c *CmdConfigure) askParam(p templates.Param) string {
var mask, required bool
if p.IsMasked() {
mask = p.Mask
}
if p.IsRequired() {
required = p.Required
}
label := p.Description.String(c.lang)
if p.Unit != "" {
label = fmt.Sprintf("%s (%s)", label, p.Unit)
}
return c.askValue(question{
label: label,
valueType: p.Type,
choice: p.Choice,
mask: mask,
required: required,
})
}
// askValue asks for value input for a given question (template param)
func (c *CmdConfigure) askValue(q question) string {
if q.valueType == templates.TypeBool {
label := q.label
if q.help != "" {
helpDescription := stripmd.Strip(q.help)
fmt.Println("-------------------------------------------------")
fmt.Println(c.localizedString("Value_Help"))
fmt.Println(helpDescription)
fmt.Println("-------------------------------------------------")
}
return c.askBoolValue(label)
}
if q.valueType == templates.TypeChoice {
label := strings.TrimSpace(strings.Join([]string{q.label, c.localizedString("Value_Choice")}, " "))
idx, _ := c.askChoice(label, q.choice)
return q.choice[idx]
}
if q.valueType == templates.TypeChargeModes {
chargingModes := []string{string(api.ModeOff), string(api.ModeNow), string(api.ModeMinPV), string(api.ModePV)}
chargeModes := []string{
c.localizedString("ChargeModeOff"),
c.localizedString("ChargeModeNow"),
c.localizedString("ChargeModeMinPV"),
c.localizedString("ChargeModePV"),
}
if !q.excludeNone {
chargingModes = append(chargingModes, "")
chargeModes = append(chargeModes, c.localizedString("ChargeModeNone"))
}
modeChoice, _ := c.askChoice(c.localizedString("ChargeMode_Question"), chargeModes)
return chargingModes[modeChoice]
}
validate := func(val any) error {
value := val.(string)
if q.invalidValues != nil && slices.Contains(q.invalidValues, value) {
return errors.New(c.localizedString("ValueError_Used"))
}
if q.choice != nil && !slices.Contains(q.choice, value) {
return errors.New(c.localizedString("ValueError_Invalid"))
}
if q.required && len(value) == 0 {
return errors.New(c.localizedString("ValueError_Empty"))
}
if !q.required && len(value) == 0 {
return nil
}
if q.valueType == templates.TypeFloat {
_, err := strconv.ParseFloat(value, 64)
if err != nil {
return errors.New(c.localizedString("ValueError_Float"))
}
}
if q.valueType == templates.TypeInt {
intValue, err := strconv.ParseInt(value, 10, 64)
if err != nil {
return errors.New(c.localizedString("ValueError_Number"))
}
if q.minNumberValue != 0 && intValue < q.minNumberValue {
return errors.New(c.localizedString("ValueError_NumberLowerThanMin", localizeMap{"Min": q.minNumberValue}))
}
if q.maxNumberValue != 0 && intValue > q.maxNumberValue {
return errors.New(c.localizedString("ValueError_NumberBiggerThanMax", localizeMap{"Max": q.maxNumberValue}))
}
}
if q.valueType == templates.TypeDuration {
_, err := time.ParseDuration(value)
if err != nil {
return errors.New(c.localizedString("ValueError_Duration"))
}
}
return nil
}
help := q.help
if q.required {
help += " (" + c.localizedString("Value_Required") + ")"
} else {
help += " (" + c.localizedString("Value_Optional") + ")"
}
if q.exampleValue != "" {
help += fmt.Sprintf(" ("+c.localizedString("Value_Sample")+": %s)", q.exampleValue)
}
var prompt survey.Prompt
if q.mask {
prompt = &survey.Password{
Message: q.label,
Help: help,
}
} else {
prompt = &survey.Input{
Message: q.label,
Default: q.defaultValue,
Help: help,
}
}
var input string
if err := c.surveyAskOne(prompt, &input, survey.WithValidator(validate)); err != nil {
c.log.FATAL.Fatal(err)
}
return input
}

View file

@ -1,42 +0,0 @@
package configure
import (
"errors"
"github.com/nicksnyder/go-i18n/v2/i18n"
)
// setDefaultTexts sets default language specific texts
func (c *CmdConfigure) setDefaultTexts() {
c.errItemNotPresent = errors.New(c.localizedString("Error_ItemNotPresent"))
c.errDeviceNotValid = errors.New(c.localizedString("Error_DeviceNotValid"))
c.updateDeviceCategoryTexts(DeviceCategoryCharger, "Category_ChargerTitle", "Category_ChargerArticle", "Category_ChargerAdditional")
c.updateDeviceCategoryTexts(DeviceCategoryGuidedSetup, "Category_SystemTitle", "Category_SystemArticle", "Category_SystemAdditional")
c.updateDeviceCategoryTexts(DeviceCategoryGridMeter, "Category_GridMeterTitle", "Category_GridMeterArticle", "Category_GridMeterAdditional")
c.updateDeviceCategoryTexts(DeviceCategoryPVMeter, "Category_PVMeterTitle", "Category_PVMeterArticle", "Category_PVMeterAdditional")
c.updateDeviceCategoryTexts(DeviceCategoryBatteryMeter, "Category_BatteryMeter", "Category_BatteryMeterArticle", "Category_BatteryMeterAdditional")
c.updateDeviceCategoryTexts(DeviceCategoryChargeMeter, "Category_ChargeMeterTitle", "Category_ChargeMeterArticle", "Category_ChargeMeterAdditional")
c.updateDeviceCategoryTexts(DeviceCategoryVehicle, "Category_VehicleTitle", "Category_VehicleArticle", "Category_VehicleAdditional")
}
// updateDeviceCategoryTexts updates the texts for a device category
func (c *CmdConfigure) updateDeviceCategoryTexts(category DeviceCategory, title, article, additional string) {
data := DeviceCategories[category]
data.title = c.localizedString(title)
data.article = c.localizedString(article)
data.additional = c.localizedString(additional)
DeviceCategories[category] = data
}
// localizedString is a helper for getting a localized string
func (c *CmdConfigure) localizedString(key string, data ...localizeMap) string {
var templateData localizeMap
if len(data) == 1 {
templateData = data[0]
}
return c.localizer.MustLocalize(&i18n.LocalizeConfig{
MessageID: key,
TemplateData: templateData,
})
}

View file

@ -1,79 +0,0 @@
package configure
import (
"github.com/evcc-io/evcc/util/templates"
)
const (
DefaultConfigFilename string = "evcc.yaml"
)
type UsageChoice string
type DeviceCategory string
func (c DeviceCategory) String() string {
return string(c)
}
const (
DeviceCategoryCharger DeviceCategory = "wallbox"
DeviceCategoryGridMeter DeviceCategory = "grid"
DeviceCategoryPVMeter DeviceCategory = "pv"
DeviceCategoryBatteryMeter DeviceCategory = "battery"
DeviceCategoryChargeMeter DeviceCategory = "charge"
DeviceCategoryVehicle DeviceCategory = "vehicle"
DeviceCategoryGuidedSetup DeviceCategory = "guided"
)
const (
defaultNameCharger = "wallbox"
defaultNameGridMeter = "grid"
defaultNamePVMeter = "pv"
defaultNameBatteryMeter = "battery"
defaultNameChargeMeter = "charge"
defaultNameVehicle = "ev"
)
type DeviceCategoryData struct {
title, article, additional string
class templates.Class
categoryFilter DeviceCategory
defaultName string
}
var DeviceCategories = map[DeviceCategory]DeviceCategoryData{
DeviceCategoryCharger: {
class: templates.Charger,
defaultName: defaultNameCharger,
},
DeviceCategoryGuidedSetup: {
class: templates.Meter,
},
DeviceCategoryGridMeter: {
class: templates.Meter,
categoryFilter: DeviceCategoryGridMeter,
defaultName: defaultNameGridMeter,
},
DeviceCategoryPVMeter: {
class: templates.Meter,
categoryFilter: DeviceCategoryPVMeter,
defaultName: defaultNamePVMeter,
},
DeviceCategoryBatteryMeter: {
class: templates.Meter,
categoryFilter: DeviceCategoryBatteryMeter,
defaultName: defaultNameBatteryMeter,
},
DeviceCategoryVehicle: {
class: templates.Vehicle,
defaultName: defaultNameVehicle,
},
DeviceCategoryChargeMeter: {
class: templates.Meter,
categoryFilter: DeviceCategoryChargeMeter,
defaultName: defaultNameChargeMeter,
},
}
type localizeMap map[string]any

3
go.mod
View file

@ -5,7 +5,6 @@ go 1.25.3
require (
dario.cat/mergo v1.0.2
github.com/AlecAivazis/survey/v2 v2.3.7
github.com/BurntSushi/toml v1.6.0
github.com/Masterminds/sprig/v3 v3.3.0
github.com/PuerkitoBio/goquery v1.11.0
github.com/WulfgarW/sensonet v0.0.7
@ -102,7 +101,6 @@ require (
github.com/traefik/yaegi v0.16.1
github.com/tv42/httpunix v0.0.0-20191220191345-2ba4b9c3382c
github.com/volkszaehler/mbmd v0.0.0-20251230155010-64e026667a91
github.com/writeas/go-strip-markdown/v2 v2.1.1
gitlab.com/bboehmke/sunny v0.16.0
go.bug.st/serial v1.6.4
go.uber.org/mock v0.6.0
@ -121,6 +119,7 @@ require (
)
require (
github.com/BurntSushi/toml v1.6.0 // indirect
github.com/Masterminds/goutils v1.1.1 // indirect
github.com/Masterminds/semver/v3 v3.3.0 // indirect
github.com/ahmetb/go-linq/v3 v3.2.0 // indirect

2
go.sum
View file

@ -768,8 +768,6 @@ github.com/volkszaehler/mbmd v0.0.0-20251230155010-64e026667a91 h1:G9VSLyJ4kTXoj
github.com/volkszaehler/mbmd v0.0.0-20251230155010-64e026667a91/go.mod h1:ZkD3hHsxHxdD2KbqQrMXQd1veeFfggok+breXT+TqWQ=
github.com/woodsbury/decimal128 v1.4.0 h1:xJATj7lLu4f2oObouMt2tgGiElE5gO6mSWUjQsBgUlc=
github.com/woodsbury/decimal128 v1.4.0/go.mod h1:BP46FUrVjVhdTbKT+XuQh2xfQaGki9LMIRJSFuh6THU=
github.com/writeas/go-strip-markdown/v2 v2.1.1 h1:hAxUM21Uhznf/FnbVGiJciqzska6iLei22Ijc3q2e28=
github.com/writeas/go-strip-markdown/v2 v2.1.1/go.mod h1:UvvgPJgn1vvN8nWuE5e7v/+qmDu3BSVnKAB6Gl7hFzA=
github.com/xiang90/probing v0.0.0-20190116061207-43a291ad63a2/go.mod h1:UETIi67q53MR2AWcXfiuqkDkRtnGDLqkBTpCHuJHxtU=
github.com/yosida95/uritemplate/v3 v3.0.2 h1:Ed3Oyj9yrmi9087+NczuL5BwkIc4wvTb5zIM+UJPGz4=
github.com/yosida95/uritemplate/v3 v3.0.2/go.mod h1:ILOh0sOhIJR3+L/8afwt/kE++YT040gmv5BQTMR2HP4=