From 023a48dd2483b9dd161689f7a47c8f21e4e87664 Mon Sep 17 00:00:00 2001 From: andig Date: Thu, 9 Jul 2026 12:43:27 +0200 Subject: [PATCH] Templates: add capabilities on product level (#31588) --- templates/README.md | 38 +++++++++++++++++++++----- util/templates/capability_test.go | 45 +++++++++++++++++++++++++++++++ util/templates/documentation.go | 2 +- util/templates/init.go | 13 +++++++++ util/templates/template.go | 2 +- util/templates/types.go | 5 ++-- 6 files changed, 95 insertions(+), 10 deletions(-) create mode 100644 util/templates/capability_test.go diff --git a/templates/README.md b/templates/README.md index 3227a4ce0..f63d72913 100644 --- a/templates/README.md +++ b/templates/README.md @@ -50,14 +50,40 @@ Note: The official website of the manufacturer or service provider is the refere ## `capabilities` -`capabilities` provides an option to define special capabilities of the device as a list of strings +`capabilities` provides an option to define special capabilities of the device as a list of strings. -**Possible Values**: +**Possible values**: -- `iso151182`: If the charger supports communicating via ISO15118-2 -- `rfid`: If the charger supports RFID -- `1p3p`: If the charger supports 1P/3P-phase switching -- `smahems`: If the device can be used as an SMA HEMS device, only used for the SMA Home Manager 2.0 right now +- `iso151182`: The device supports communicating via ISO 15118-2. +- `mA`: The device supports granular (milliamp) current control. +- `rfid`: The device supports RFID. +- `1p3p`: The device supports 1P/3P phase switching. +- `battery-control`: The device supports battery control. +- `meter`: The device has a built-in energy meter. +- `dim`: The device supports EnWG §14a dimming. +- `curtail`: The device supports EEG §9 curtailment. + +`capabilities` can be set at two levels: + +- **Template level** (next to `template`): applies to every product. +- **Product level** (under a `products` entry): applies to that product only. + +Both levels are merged, not overwritten: template-level capabilities are appended to each product's own list. A product cannot remove an inherited capability, and duplicates are rejected, so do not repeat a template-level capability on a product. + +**Example** (all products get `1p3p`; only the second product additionally gets `meter`): + +```yaml +template: demo-charger +capabilities: ["1p3p"] +products: + - brand: Demo + description: + generic: Basic + - brand: Demo + description: + generic: Plus + capabilities: ["meter"] # effective: ["meter", "1p3p"] +``` ## `requirements` diff --git a/util/templates/capability_test.go b/util/templates/capability_test.go new file mode 100644 index 000000000..f3df5b289 --- /dev/null +++ b/util/templates/capability_test.go @@ -0,0 +1,45 @@ +package templates + +import ( + "testing" + + "github.com/stretchr/testify/require" +) + +func TestCapabilityPushdown(t *testing.T) { + tmpl, err := fromBytes([]byte(` +template: caps-demo +capabilities: ["1p3p"] +products: + - brand: Plain + - brand: Extra + capabilities: ["meter"] +params: + - name: host + required: true +render: | + type: demo +`)) + require.NoError(t, err) + + // template caps appended to product caps (product first) + require.Equal(t, []Capability{Capability1p3p}, tmpl.Products[0].Capabilities) + require.Equal(t, []Capability{CapabilityMeter, Capability1p3p}, tmpl.Products[1].Capabilities) +} + +func TestCapabilityDuplicate(t *testing.T) { + // product repeats a template-level capability + _, err := fromBytes([]byte(` +template: caps-dup +capabilities: ["1p3p"] +products: + - brand: Dup + capabilities: ["1p3p"] +params: + - name: host + required: true +render: | + type: demo +`)) + require.ErrorContains(t, err, "duplicate capability '1p3p'") +} diff --git a/util/templates/documentation.go b/util/templates/documentation.go index 72990a076..56205594b 100644 --- a/util/templates/documentation.go +++ b/util/templates/documentation.go @@ -101,7 +101,7 @@ func (t *Template) RenderDocumentation(product Product, lang string) ([]byte, er "ProductBrand": product.Brand, "ProductDescription": product.Description.String(lang), "ProductGroup": t.GroupTitle(lang), - "Capabilities": t.Capabilities, + "Capabilities": product.Capabilities, "Countries": t.Countries, "Requirements": t.Requirements.EVCC, "RequirementDescription": t.Requirements.Description.String(lang), diff --git a/util/templates/init.go b/util/templates/init.go index c3083a7a8..5e9cc3165 100644 --- a/util/templates/init.go +++ b/util/templates/init.go @@ -79,6 +79,19 @@ func fromBytes(b []byte) (Template, error) { } } + // push down capabilities to products + for i := range tmpl.Products { + tmpl.Products[i].Capabilities = append(tmpl.Products[i].Capabilities, tmpl.Capabilities...) + + seen := make(map[Capability]struct{}, len(tmpl.Products[i].Capabilities)) + for _, c := range tmpl.Products[i].Capabilities { + if _, ok := seen[c]; ok { + return Template{}, fmt.Errorf("template '%s': duplicate capability '%s' for product '%s'", tmpl.Template, c, tmpl.Products[i].Identifier()) + } + seen[c] = struct{}{} + } + } + return tmpl, nil } diff --git a/util/templates/template.go b/util/templates/template.go index a16c4084c..bd93f93f8 100644 --- a/util/templates/template.go +++ b/util/templates/template.go @@ -22,7 +22,7 @@ type Template struct { Group string `json:",omitempty"` // the group this template belongs to, references groupList entries Covers []string `json:",omitempty"` // list of covered outdated template names Products []Product `json:",omitempty"` // list of products this template is compatible with - Capabilities []Capability `json:",omitempty"` + Capabilities []Capability `json:"-"` Countries []CountryCode `json:",omitempty"` // list of countries supported by this template Requirements Requirements `json:",omitempty"` Caveats []Caveat `json:",omitempty"` // known device limitations diff --git a/util/templates/types.go b/util/templates/types.go index 8ff5739d7..93ca81c05 100644 --- a/util/templates/types.go +++ b/util/templates/types.go @@ -321,8 +321,9 @@ func (p Param) MarshalJSON() ([]byte, error) { // Product contains naming information about a product a template supports type Product struct { - Brand string // product brand - Description TextLanguage `json:",omitempty"` // product name + Brand string // product brand + Description TextLanguage `json:",omitempty"` // product name + Capabilities []Capability `json:",omitempty"` // appended to template-level capabilities } // Title returns the product title in the given language