package templates import ( "bytes" "embed" "fmt" "io/fs" "os" "slices" "sync" "text/template" "github.com/evcc-io/evcc/templates/definition" "github.com/samber/lo" "go.yaml.in/yaml/v4" ) // IncludeExt is the file extension of class-local, include-only templates const IncludeExt = ".tpl" var ( //go:embed includes/*.tpl includeFS embed.FS // baseTmpl holds all included template definitions baseTmpl *template.Template // classTmpl holds baseTmpl plus the class-local include definitions classTmpl = make(map[Class]*template.Template) templates = make(map[Class][]Template) ConfigDefaults configDefaults mu sync.Mutex encoderLanguage string ) func init() { ConfigDefaults.Load() // the root template is replaced by the rendered template's body, so its name // must not collide with an include name (`$` cannot appear in a file name) baseTmpl = template.Must(FuncMap(template.New("$root")).ParseFS(includeFS, "includes/*.tpl")) for _, class := range []Class{Charger, Meter, Vehicle, Tariff, Messenger, Circuit, Hems, Curtailer} { if err := loadIncludes(definition.TemplateIncludes, class); err != nil { panic(err) } load(class) } } // loadIncludes parses the class-local include-only templates func loadIncludes(fsys fs.FS, class Class) error { files, err := fs.Glob(fsys, class.String()+"/*"+IncludeExt) if err != nil || len(files) == 0 { return err } tmpl, err := FuncMap(template.Must(baseTmpl.Clone())).ParseFS(fsys, files...) if err != nil { return fmt.Errorf("processing %s includes failed: %w", class, err) } classTmpl[class] = tmpl return nil } // Register adds a template file to the registry func Register(class Class, filepath string) error { b, err := os.ReadFile(filepath) if err != nil { return err } tmpl, err := fromBytes(b) if err != nil { return fmt.Errorf("processing template '%s' failed: %w", filepath, err) } return register(class, tmpl) } func register(class Class, tmpl Template) error { if slices.ContainsFunc(templates[class], func(t Template) bool { return t.Template == tmpl.Template }) { return fmt.Errorf("duplicate template name: %s", tmpl.Template) } templates[class] = append(templates[class], tmpl) return nil } func fromBytes(b []byte) (Template, error) { // error on unknown fields dec := yaml.NewDecoder(bytes.NewReader(b)) dec.KnownFields(true) var tmpl Template if err := dec.Decode(&tmpl); err != nil { return Template{}, err } for _, f := range []func() error{tmpl.ResolvePresets, tmpl.ResolveGroup, tmpl.UpdateParamsWithDefaults, tmpl.UpdateModbusParamsWithDefaults, tmpl.SortRequiredParamsFirst, tmpl.Validate} { if err := f(); err != nil { return tmpl, fmt.Errorf("template '%s': %w", tmpl.Template, err) } } // 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 } func load(class Class) { err := fs.WalkDir(definition.YamlTemplates, class.String(), func(filepath string, d fs.DirEntry, err error) error { if err != nil { return err } if d.IsDir() { return nil } b, err := fs.ReadFile(definition.YamlTemplates, filepath) if err != nil { return err } tmpl, err := fromBytes(b) if err != nil { return fmt.Errorf("processing template '%s' failed: %w", filepath, err) } return register(class, tmpl) }) if err != nil { panic(err) } } // EncoderLanguage sets the template language for encoding json func EncoderLanguage(lang string) { mu.Lock() defer mu.Unlock() encoderLanguage = lang } type filterFunc func([]Template) []Template // WithDeprecated returns a filterFunc that includes all templates func WithDeprecated() filterFunc { return func(t []Template) []Template { return t } } // ByClass returns templates for class excluding deprecated templates func ByClass(class Class, opt ...filterFunc) []Template { res := templates[class] if len(opt) == 0 { opt = append(opt, func(t []Template) []Template { return lo.Filter(t, func(t Template, _ int) bool { return !t.Deprecated }) }) } for _, o := range opt { res = o(res) } return res } // ByClass returns templates for class and name including deprecated templates func ByName(class Class, name string) (Template, error) { for _, tmpl := range templates[class] { if tmpl.Template == name || slices.Contains(tmpl.Covers, name) { return tmpl, nil } } return Template{}, fmt.Errorf("template not found: %s", name) }