diff --git a/go.mod b/go.mod index 7bf9f24dc..309b0a206 100644 --- a/go.mod +++ b/go.mod @@ -45,6 +45,7 @@ require ( github.com/google/uuid v1.6.0 github.com/gorilla/handlers v1.5.2 github.com/gorilla/mux v1.8.1 + github.com/gosimple/slug v1.15.0 github.com/gregdel/pushover v1.3.1 github.com/gregjones/httpcache v0.0.0-20190611155906-901d90724c79 github.com/grid-x/modbus v0.0.0-20250516072809-4b99c910e8e7 @@ -151,6 +152,7 @@ require ( github.com/google/pprof v0.0.0-20250317173921-a4b03ec1a45e // indirect github.com/google/renameio/v2 v2.0.0 // indirect github.com/gorilla/websocket v1.5.3 // indirect + github.com/gosimple/unidecode v1.0.1 // indirect github.com/grid-x/serial v0.0.0-20211107191517-583c7356b3aa // indirect github.com/huandu/xstrings v1.5.0 // indirect github.com/inconshreveable/mousetrap v1.1.0 // indirect diff --git a/go.sum b/go.sum index 9dc8f2a1c..c26aaaf1e 100644 --- a/go.sum +++ b/go.sum @@ -308,6 +308,10 @@ github.com/gorilla/mux v1.8.1/go.mod h1:AKf9I4AEqPTmMytcMc0KkNouC66V3BtZ4qD5fmWS github.com/gorilla/websocket v0.0.0-20170926233335-4201258b820c/go.mod h1:E7qHFY5m1UJ88s3WnNqhKjPHQ0heANvMoAMk2YaljkQ= github.com/gorilla/websocket v1.5.3 h1:saDtZ6Pbx/0u+bgYQ3q96pZgCzfhKXGPqt7kZ72aNNg= github.com/gorilla/websocket v1.5.3/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= +github.com/gosimple/slug v1.15.0 h1:wRZHsRrRcs6b0XnxMUBM6WK1U1Vg5B0R7VkIf1Xzobo= +github.com/gosimple/slug v1.15.0/go.mod h1:UiRaFH+GEilHstLUmcBgWcI42viBN7mAb818JrYOeFQ= +github.com/gosimple/unidecode v1.0.1 h1:hZzFTMMqSswvf0LBJZCZgThIZrpDHFXux9KeGmn6T/o= +github.com/gosimple/unidecode v1.0.1/go.mod h1:CP0Cr1Y1kogOtx0bJblKzsVWrqYaqfNOnHzpgWw4Awc= github.com/gregdel/pushover v1.3.1 h1:4bMLITOZ15+Zpi6qqoGqOPuVHCwSUvMCgVnN5Xhilfo= github.com/gregdel/pushover v1.3.1/go.mod h1:EcaO66Nn1StkpEm1iKtBTV3d2A16SoMsVER1PthX7to= github.com/gregjones/httpcache v0.0.0-20190611155906-901d90724c79 h1:+ngKgrYPPJrOjhax5N+uePQ0Fh1Z7PheYoUI/0nzkPA= diff --git a/templates/definition/charger/ocpp-abb-tac.yaml b/templates/definition/charger/ocpp-abb-tac.yaml index ec55c1da4..b23135099 100644 --- a/templates/definition/charger/ocpp-abb-tac.yaml +++ b/templates/definition/charger/ocpp-abb-tac.yaml @@ -3,7 +3,7 @@ covers: ["ocpp-abb"] products: - brand: ABB description: - generic: Terra AC + generic: Terra AC (OCPP) capabilities: ["mA", "rfid"] requirements: evcc: ["sponsorship", "skiptest"] diff --git a/templates/definition/charger/ocpp-alfen.yaml b/templates/definition/charger/ocpp-alfen.yaml index aa5bac0ee..cc1c7865e 100644 --- a/templates/definition/charger/ocpp-alfen.yaml +++ b/templates/definition/charger/ocpp-alfen.yaml @@ -2,7 +2,7 @@ template: ocpp-alfen products: - brand: Alfen description: - generic: Eve + generic: Eve (OCPP) capabilities: ["mA", "rfid", "1p3p"] requirements: evcc: ["sponsorship", "skiptest"] diff --git a/templates/definition/charger/ocpp-goe.yaml b/templates/definition/charger/ocpp-goe.yaml index e865fc5fe..aeafbede3 100644 --- a/templates/definition/charger/ocpp-goe.yaml +++ b/templates/definition/charger/ocpp-goe.yaml @@ -3,16 +3,16 @@ covers: ["ocpp-fronius-wattpilot"] products: - brand: go-e description: - generic: Charger V3 + generic: Charger V3 (OCPP) - brand: go-e description: - generic: Charger Gemini + generic: Charger Gemini (OCPP) - brand: go-e description: - generic: Charger PRO + generic: Charger PRO (OCPP) - brand: Fronius description: - generic: Wattpilot + generic: Wattpilot (OCPP) capabilities: ["rfid", "1p3p"] requirements: evcc: ["sponsorship", "skiptest"] diff --git a/templates/definition/charger/ocpp-zaptec.yaml b/templates/definition/charger/ocpp-zaptec.yaml index 34e9f31dc..741841b3c 100644 --- a/templates/definition/charger/ocpp-zaptec.yaml +++ b/templates/definition/charger/ocpp-zaptec.yaml @@ -2,7 +2,7 @@ template: ocpp-zaptec products: - brand: Zaptec description: - generic: Go + generic: Go (OCPP) capabilities: ["rfid"] requirements: description: diff --git a/util/templates/documentation.go b/util/templates/documentation.go index f12f6f786..fabb39896 100644 --- a/util/templates/documentation.go +++ b/util/templates/documentation.go @@ -43,6 +43,7 @@ func (t *Template) RenderDocumentation(product Product, lang string) ([]byte, er } var modbusRender string + modbusData := make(map[string]interface{}) if modbusChoices := t.ModbusChoices(); len(modbusChoices) > 0 { if i, _ := t.ParamByName(ParamModbus); i > -1 { modbusTmpl, err := template.New("yaml").Funcs(sprig.FuncMap()).Parse(documentationModbusTmpl) @@ -50,7 +51,6 @@ func (t *Template) RenderDocumentation(product Product, lang string) ([]byte, er panic(err) } - modbusData := make(map[string]interface{}) t.ModbusValues(RenderModeDocs, modbusData) out := new(bytes.Buffer) @@ -91,6 +91,7 @@ func (t *Template) RenderDocumentation(product Product, lang string) ([]byte, er data := map[string]interface{}{ "Template": t.Template, + "ProductIdentifier": product.Identifier(), "ProductBrand": product.Brand, "ProductDescription": product.Description.String(lang), "ProductGroup": t.GroupTitle(lang), @@ -102,6 +103,7 @@ func (t *Template) RenderDocumentation(product Product, lang string) ([]byte, er "AdvancedParams": hasAdvancedParams, "Usages": t.Usages(), "Modbus": modbusRender, + "ModbusData": modbusData, } out := new(bytes.Buffer) diff --git a/util/templates/documentation.tpl b/util/templates/documentation.tpl index 5a7941fcf..fc2c7956b 100644 --- a/util/templates/documentation.tpl +++ b/util/templates/documentation.tpl @@ -4,8 +4,8 @@ - {{ . }} {{- end }} {{- $unit := .Unit -}} - {{- $description := localize .Description | replace "\n" " " -}} - {{- $help := localize .Help | replace "\n" " " -}} + {{- $description := localize .Description | replace "\n" " " | trim -}} + {{- $help := localize .Help | replace "\n" " " | trim -}} {{- $choices := join ", " .Choice -}} {{- $optional := not .IsRequired -}} {{- if or $help $choices $optional $description }} # {{end}} @@ -52,7 +52,9 @@ {{- end }} {{- end -}} +template: {{ .Template }} product: + identifier: {{ .ProductIdentifier }} {{- if .ProductBrand }} brand: {{ .ProductBrand }} {{- end }} @@ -96,3 +98,25 @@ render: {{- include "advanced" . | indent 4 }} {{- end }} {{- end }} +params: + {{- range .Params }} + {{- if and (not (eq .Name "usage")) (not .IsDeprecated) }} + - name: {{ .Name | quote }} + example: {{ .Example | quote }} + default: {{ .Default }} + choice: [{{ join ", " .Choice }}] + unit: {{ .Unit }} + {{- $description := localize .Description | replace "\n" " " | trim }} + description: {{ $description | quote }} + {{- $help := localize .Help | replace "\n" " " | trim }} + help: {{ $help | quote }} + advanced: {{ .IsAdvanced }} + optional: {{ not .IsRequired }} + {{- end }} + {{- end }} +{{- if .ModbusData }} +modbus: +{{- range $key, $value := .ModbusData }} + {{ $key }}: {{ $value }} +{{- end }} +{{- end }} \ No newline at end of file diff --git a/util/templates/generate/main.go b/util/templates/generate/main.go index aef520120..776b81e73 100644 --- a/util/templates/generate/main.go +++ b/util/templates/generate/main.go @@ -9,16 +9,20 @@ import ( "strings" "github.com/evcc-io/evcc/util/templates" + "github.com/gosimple/slug" ) const ( docsPath = "../../../templates/docs" websitePath = "../../../templates/evcc.io" + iconsPath = "../../../templates/icons" ) //go:generate go run main.go func main() { + slug.CustomSub = map[string]string{"+": "plus"} + for _, lang := range []string{"de", "en"} { if err := generateDocs(lang); err != nil { panic(err) @@ -28,6 +32,10 @@ func main() { if err := generateBrandJSON(); err != nil { panic(err) } + + if err := generateProductJSON(); err != nil { + panic(err) + } } func generateDocs(lang string) error { @@ -57,7 +65,7 @@ func generateClass(class templates.Class, lang string) error { return err } - for index, product := range tmpl.Products { + for _, product := range tmpl.Products { fmt.Println(tmpl.Template + ": " + product.Title(lang)) b, err := tmpl.RenderDocumentation(product, lang) @@ -65,7 +73,12 @@ func generateClass(class templates.Class, lang string) error { return err } - filename := fmt.Sprintf("%s/%s/%s/%s_%d.yaml", docsPath, lang, strings.ToLower(class.String()), tmpl.Template, index) + filename := fmt.Sprintf("%s/%s/%s/%s.yaml", docsPath, lang, strings.ToLower(class.String()), product.Identifier()) + + if _, err := os.Stat(filename); err == nil { + return fmt.Errorf("file already exists: %s - product titles must be unique", filename) + } + if err := os.WriteFile(filename, b, 0o644); err != nil { return err } @@ -164,3 +177,39 @@ func generateBrandJSON() error { return err } + +func generateProductJSON() error { + type ProductInfo struct { + Brand string `json:"brand"` + Description string `json:"description"` + } + + products := make(map[string]map[string]ProductInfo) + + for _, class := range templates.ClassValues() { + classKey := strings.ToLower(class.String()) + products[classKey] = make(map[string]ProductInfo) + + for _, tmpl := range templates.ByClass(class) { + for _, product := range tmpl.Products { + products[classKey][product.Identifier()] = ProductInfo{ + Brand: product.Brand, + Description: product.Description.String("en"), + } + } + } + } + + if _, err := os.Stat(iconsPath); os.IsNotExist(err) { + if err := os.MkdirAll(iconsPath, 0o755); err != nil { + return err + } + } + + file, err := json.MarshalIndent(products, "", " ") + if err == nil { + err = os.WriteFile(iconsPath+"/products.json", file, 0o644) + } + + return err +} diff --git a/util/templates/types.go b/util/templates/types.go index 1569c4fb9..5cad711b1 100644 --- a/util/templates/types.go +++ b/util/templates/types.go @@ -8,6 +8,7 @@ import ( "strings" "dario.cat/mergo" + "github.com/gosimple/slug" ) const ( @@ -259,10 +260,16 @@ type Product struct { Description TextLanguage `json:",omitempty"` // product name } +// Title returns the product title in the given language func (p Product) Title(lang string) string { return strings.TrimSpace(fmt.Sprintf("%s %s", p.Brand, p.Description.String(lang))) } +// Identifier returns a unique language-independent identifier for the product +func (p Product) Identifier() string { + return slug.Make(p.Title("en")) +} + type CountryCode string func (c CountryCode) IsValid() bool {