diff --git a/assets/js/components/GlobalSettingsModal.vue b/assets/js/components/GlobalSettingsModal.vue index c03a3dfb7..6fa7f72ee 100644 --- a/assets/js/components/GlobalSettingsModal.vue +++ b/assets/js/components/GlobalSettingsModal.vue @@ -83,6 +83,26 @@ + +
+ +
+ +
+
+
@@ -99,6 +119,7 @@ import { getLocalePreference, setLocalePreference, LOCALES, removeLocalePreferen import { getThemePreference, setThemePreference, THEMES } from "../theme"; import { getUnits, setUnits, UNITS } from "../units"; import { getGridDetails, setGridDetails, GRID_DETAILS } from "../gridDetails"; +import { getHiddenFeatures, setHiddenFeatures } from "../featureflags"; export default { name: "GlobalSettingsModal", @@ -114,6 +135,7 @@ export default { language: getLocalePreference() || "", unit: getUnits(), gridDetails: getGridDetails(), + hiddenFeatures: getHiddenFeatures(), THEMES, UNITS, GRID_DETAILS, @@ -128,6 +150,9 @@ export default { locales.sort((a, b) => (a.name < b.name ? -1 : 1)); return locales; }, + isNightly: () => { + return !!window.evcc.commit; + }, }, watch: { unit(value) { @@ -139,6 +164,9 @@ export default { theme(value) { setThemePreference(value); }, + hiddenFeatures(value) { + setHiddenFeatures(value); + }, language(value) { const i18n = this.$root.$i18n; if (value) { diff --git a/assets/js/components/Loadpoint.vue b/assets/js/components/Loadpoint.vue index 61da4ac00..72bbbba64 100644 --- a/assets/js/components/Loadpoint.vue +++ b/assets/js/components/Loadpoint.vue @@ -201,7 +201,7 @@ export default { return this.collectProps(LoadpointSettingsModal); }, settingsButtonVisible: function () { - return this.$hiddenFeatures || [0, 1, 3].includes(this.phasesConfigured); + return this.$hiddenFeatures() || [0, 1, 3].includes(this.phasesConfigured); }, vehicle: function () { return this.collectProps(Vehicle); diff --git a/assets/js/components/LoadpointSettingsModal.vue b/assets/js/components/LoadpointSettingsModal.vue index 9b44edc2f..21354e97d 100644 --- a/assets/js/components/LoadpointSettingsModal.vue +++ b/assets/js/components/LoadpointSettingsModal.vue @@ -152,7 +152,7 @@ -
+
-
+
@@ -28,9 +28,9 @@ import "@h2d2/shopicons/es/regular/arrowup"; import TopNavigation from "./TopNavigation.vue"; import Notifications from "./Notifications.vue"; +import VehcileSettingsModal from "./VehicleSettingsModal.vue"; import Energyflow from "./Energyflow/Energyflow.vue"; import Loadpoints from "./Loadpoints.vue"; -import Vehicles from "./Vehicles.vue"; import Footer from "./Footer.vue"; import formatter from "../mixins/formatter"; import collector from "../mixins/collector"; @@ -43,7 +43,7 @@ export default { Footer, Notifications, TopNavigation, - Vehicles, + VehcileSettingsModal, }, mixins: [formatter, collector], props: { diff --git a/assets/js/components/Vehicle.vue b/assets/js/components/Vehicle.vue index 1c314b346..37cd6ffa9 100644 --- a/assets/js/components/Vehicle.vue +++ b/assets/js/components/Vehicle.vue @@ -54,9 +54,6 @@ @target-energy-updated="targetEnergyUpdated" />
-
- vor 5 Stunden -
diff --git a/assets/js/components/VehicleOptions.vue b/assets/js/components/VehicleOptions.vue index 77e2f0709..4cb19ffc9 100644 --- a/assets/js/components/VehicleOptions.vue +++ b/assets/js/components/VehicleOptions.vue @@ -23,6 +23,17 @@ {{ $t("main.vehicle.unknown") }} +
+ +
  • + +
  • +
  • + +
  • +
    @@ -30,6 +41,7 @@ diff --git a/assets/js/components/VehicleSettingsModal.vue b/assets/js/components/VehicleSettingsModal.vue new file mode 100644 index 000000000..32db982ec --- /dev/null +++ b/assets/js/components/VehicleSettingsModal.vue @@ -0,0 +1,198 @@ + + + + diff --git a/assets/js/components/Vehicles.vue b/assets/js/components/Vehicles.vue deleted file mode 100644 index 480e02d61..000000000 --- a/assets/js/components/Vehicles.vue +++ /dev/null @@ -1,102 +0,0 @@ - - - - diff --git a/assets/js/experimental.js b/assets/js/experimental.js new file mode 100644 index 000000000..8a53747fc --- /dev/null +++ b/assets/js/experimental.js @@ -0,0 +1,27 @@ +import settings from "./settings"; + +const KM = "km"; +const MILES = "mi"; +export const UNITS = [KM, MILES]; + +const MILES_FACTOR = 0.6213711922; + +function isMiles() { + return settings.unit === MILES; +} + +export function distanceValue(value) { + return isMiles() ? value * MILES_FACTOR : value; +} + +export function distanceUnit() { + return isMiles() ? "mi" : "km"; +} + +export function getUnits() { + return isMiles() ? MILES : KM; +} + +export function setUnits(value) { + settings.unit = value; +} diff --git a/assets/js/featureflags.js b/assets/js/featureflags.js index e61de4290..7932143e9 100644 --- a/assets/js/featureflags.js +++ b/assets/js/featureflags.js @@ -1,5 +1,15 @@ +import settings from "./settings"; + +export function setHiddenFeatures(value) { + settings.hiddenFeatures = value; +} + +export function getHiddenFeatures() { + return settings.hiddenFeatures; +} + export default { install: (app) => { - app.config.globalProperties.$hiddenFeatures = window.localStorage["hidden_features"] === "true"; + app.config.globalProperties.$hiddenFeatures = getHiddenFeatures; }, }; diff --git a/assets/js/settings.js b/assets/js/settings.js index 903148474..fbe3b0eae 100644 --- a/assets/js/settings.js +++ b/assets/js/settings.js @@ -3,6 +3,7 @@ import { reactive, watch } from "vue"; const SETTINGS_LOCALE = "settings_locale"; const SETTINGS_THEME = "settings_theme"; const SETTINGS_UNIT = "settings_unit"; +const SETTINGS_HIDDEN_FEATURES = "settings_hidden_features"; const SETTINGS_ENERGYFLOW_DETAILS = "settings_energyflow_details"; const SETTINGS_GRID_DETAILS = "settings_grid_details"; @@ -40,6 +41,7 @@ const settings = reactive({ theme: read(SETTINGS_THEME), unit: read(SETTINGS_UNIT), gridDetails: read(SETTINGS_GRID_DETAILS), + hiddenFeatures: readBool(SETTINGS_HIDDEN_FEATURES), energyflowDetails: readBool(SETTINGS_ENERGYFLOW_DETAILS), }); @@ -47,6 +49,7 @@ watch(() => settings.locale, save(SETTINGS_LOCALE)); watch(() => settings.theme, save(SETTINGS_THEME)); watch(() => settings.unit, save(SETTINGS_UNIT)); watch(() => settings.gridDetails, save(SETTINGS_GRID_DETAILS)); +watch(() => settings.hiddenFeatures, saveBool(SETTINGS_HIDDEN_FEATURES)); watch(() => settings.energyflowDetails, saveBool(SETTINGS_ENERGYFLOW_DETAILS)); export default settings; diff --git a/cmd/configure/configure.go b/cmd/configure/configure.go index a04b270bc..b3ce319e7 100644 --- a/cmd/configure/configure.go +++ b/cmd/configure/configure.go @@ -88,7 +88,7 @@ func (c *Configure) DevicesOfClass(class templates.Class) []device { case templates.Vehicle: return c.config.Vehicles default: - panic("invalid class: " + class) + panic("invalid class: " + class.String()) } } diff --git a/cmd/configure/helper.go b/cmd/configure/helper.go index 1da400d04..bb782956f 100644 --- a/cmd/configure/helper.go +++ b/cmd/configure/helper.go @@ -431,8 +431,8 @@ func (c *CmdConfigure) processParams(templateItem *templates.Template, deviceCat continue } - switch param.ValueType { - case templates.ParamValueTypeStringList: + switch param.Type { + case templates.ParamTypeStringList: values := c.processListInputConfig(param) var nonEmptyValues []string for _, value := range values { @@ -493,13 +493,13 @@ func (c *CmdConfigure) processInputConfig(param templates.Param) string { defaultValue: param.Default, exampleValue: param.Example, help: help, - valueType: param.ValueType, + valueType: param.Type, validValues: param.ValidValues, mask: param.IsMask(), required: param.IsRequired(), }) - if param.ValueType == templates.ParamValueTypeBool && value == "true" { + if param.Type == templates.ParamTypeBool && value == "true" { if err := c.processParamRequirements(param); err != nil { return "false" } diff --git a/cmd/configure/main.go b/cmd/configure/main.go index c6625212f..7ca30b1a6 100644 --- a/cmd/configure/main.go +++ b/cmd/configure/main.go @@ -352,7 +352,7 @@ func (c *CmdConfigure) configureLoadpoints() { fmt.Println() minAmperage := c.askValue(question{ label: c.localizedString("Loadpoint_WallboxMinAmperage", nil), - valueType: templates.ParamValueTypeNumber, + valueType: templates.ParamTypeNumber, minNumberValue: int64(minValue), maxNumberValue: 32, required: true, @@ -360,7 +360,7 @@ func (c *CmdConfigure) configureLoadpoints() { loadpoint.MinCurrent, _ = strconv.Atoi(minAmperage) maxAmperage := c.askValue(question{ label: c.localizedString("Loadpoint_WallboxMaxAmperage", nil), - valueType: templates.ParamValueTypeNumber, + valueType: templates.ParamTypeNumber, minNumberValue: 6, maxNumberValue: 32, required: true, @@ -402,7 +402,7 @@ func (c *CmdConfigure) configureLoadpoints() { case 3: amperage := c.askValue(question{ label: c.localizedString("Loadpoint_WallboxMaxAmperage", nil), - valueType: templates.ParamValueTypeNumber, + valueType: templates.ParamTypeNumber, minNumberValue: int64(minValue), maxNumberValue: 32, required: true, @@ -419,12 +419,12 @@ func (c *CmdConfigure) configureLoadpoints() { } fmt.Println() - loadpoint.Mode = c.askValue(question{valueType: templates.ParamValueTypeChargeModes, excludeNone: true}) + loadpoint.Mode = c.askValue(question{valueType: templates.ParamTypeChargeModes, excludeNone: true}) fmt.Println() loadpoint.ResetOnDisconnect = c.askValue(question{ label: c.localizedString("Loadpoint_ResetOnDisconnect", nil), - valueType: templates.ParamValueTypeBool, + valueType: templates.ParamTypeBool, }) c.configuration.AddLoadpoint(loadpoint) diff --git a/cmd/configure/survey.go b/cmd/configure/survey.go index 1b97b33ae..f297c71d4 100644 --- a/cmd/configure/survey.go +++ b/cmd/configure/survey.go @@ -122,7 +122,7 @@ func (c *CmdConfigure) askBoolValue(label string) string { // askValue asks for value input for a given question (template param) func (c *CmdConfigure) askValue(q question) string { - if q.valueType == templates.ParamValueTypeBool { + if q.valueType == templates.ParamTypeBool { label := q.label if q.help != "" { helpDescription := stripmd.Strip(q.help) @@ -135,7 +135,7 @@ func (c *CmdConfigure) askValue(q question) string { return c.askBoolValue(label) } - if q.valueType == templates.ParamValueTypeChargeModes { + if q.valueType == templates.ParamTypeChargeModes { chargingModes := []string{string(api.ModeOff), string(api.ModeNow), string(api.ModeMinPV), string(api.ModePV)} chargeModes := []string{ c.localizedString("ChargeModeOff", nil), @@ -169,14 +169,14 @@ func (c *CmdConfigure) askValue(q question) string { return nil } - if q.valueType == templates.ParamValueTypeFloat { + if q.valueType == templates.ParamTypeFloat { _, err := strconv.ParseFloat(value, 64) if err != nil { return errors.New(c.localizedString("ValueError_Float", nil)) } } - if q.valueType == templates.ParamValueTypeNumber { + if q.valueType == templates.ParamTypeNumber { intValue, err := strconv.ParseInt(value, 10, 64) if err != nil { return errors.New(c.localizedString("ValueError_Number", nil)) @@ -189,7 +189,7 @@ func (c *CmdConfigure) askValue(q question) string { } } - if q.valueType == templates.ParamValueTypeDuration { + if q.valueType == templates.ParamTypeDuration { _, err := time.ParseDuration(value) if err != nil { return errors.New(c.localizedString("ValueError_Duration", nil)) diff --git a/i18n/en.toml b/i18n/en.toml index 54d4099b5..159053ebc 100644 --- a/i18n/en.toml +++ b/i18n/en.toml @@ -158,9 +158,11 @@ label = "Limit" noLimit = "none" [main.vehicle] -changeVehicle = "Change Vehicle" +addVehicle = "Add vehicle" +changeVehicle = "Change vehicle" detectionActive = "Detecting vehicle…" fallbackName = "Vehicle" +moreActions = "More actions" none = "No vehicle" targetSoc = "Limit" unknown = "Guest vehicle" @@ -236,6 +238,10 @@ label = "Grid details" none = "none" price = "price" +[settings.hiddenFeatures] +label = "Experimental" +value = "Show experimental UI features." + [settings.language] auto = "Automatic" label = "Language" @@ -265,3 +271,8 @@ lineError = "Error in {0}." lineErrorLink = "line {0}" restartButton = "Restart" title = "Startup Error" + +[vehicleSettings] +cancel = "Cancel" +template = "Manufacturer" +test = "Test" diff --git a/package-lock.json b/package-lock.json index 43be5edf4..ab588b1b5 100644 --- a/package-lock.json +++ b/package-lock.json @@ -26,6 +26,7 @@ "eslint-plugin-promise": "^6.0.0", "eslint-plugin-vue": "^9.3.0", "histoire": "^0.12.4", + "json-to-pretty-yaml": "^1.2.2", "markty-toml": "^0.1.1", "prettier": "^2.5.1", "prettier-plugin-toml": "^0.3.1", @@ -3816,6 +3817,18 @@ "resolved": "https://registry.npmjs.org/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz", "integrity": "sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==" }, + "node_modules/json-to-pretty-yaml": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/json-to-pretty-yaml/-/json-to-pretty-yaml-1.2.2.tgz", + "integrity": "sha512-rvm6hunfCcqegwYaG5T4yKJWxc9FXFgBVrcTZ4XfSVRwa5HA/Xs+vB/Eo9treYYHCeNM0nrSUr82V/M31Urc7A==", + "dependencies": { + "remedial": "^1.0.7", + "remove-trailing-spaces": "^1.0.6" + }, + "engines": { + "node": ">= 0.2.0" + } + }, "node_modules/json5": { "version": "2.2.3", "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz", @@ -4640,6 +4653,19 @@ "url": "https://github.com/sponsors/mysticatea" } }, + "node_modules/remedial": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/remedial/-/remedial-1.0.8.tgz", + "integrity": "sha512-/62tYiOe6DzS5BqVsNpH/nkGlX45C/Sp6V+NtiN6JQNS1Viay7cWkazmRkrQrdFj2eshDe96SIQNIoMxqhzBOg==", + "engines": { + "node": "*" + } + }, + "node_modules/remove-trailing-spaces": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/remove-trailing-spaces/-/remove-trailing-spaces-1.0.8.tgz", + "integrity": "sha512-O3vsMYfWighyFbTd8hk8VaSj9UAGENxAtX+//ugIst2RMk5e03h6RoIS+0ylsFxY1gvmPuAY/PO4It+gPEeySA==" + }, "node_modules/require-directory": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", @@ -8657,6 +8683,15 @@ "resolved": "https://registry.npmjs.org/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz", "integrity": "sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==" }, + "json-to-pretty-yaml": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/json-to-pretty-yaml/-/json-to-pretty-yaml-1.2.2.tgz", + "integrity": "sha512-rvm6hunfCcqegwYaG5T4yKJWxc9FXFgBVrcTZ4XfSVRwa5HA/Xs+vB/Eo9treYYHCeNM0nrSUr82V/M31Urc7A==", + "requires": { + "remedial": "^1.0.7", + "remove-trailing-spaces": "^1.0.6" + } + }, "json5": { "version": "2.2.3", "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz", @@ -9260,6 +9295,16 @@ "resolved": "https://registry.npmjs.org/regexpp/-/regexpp-3.2.0.tgz", "integrity": "sha512-pq2bWo9mVD43nbts2wGv17XLiNLya+GklZ8kaDLV2Z08gDCsGpnKn9BFMepvWuHCbyVvY7J5o5+BVvoQbmlJLg==" }, + "remedial": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/remedial/-/remedial-1.0.8.tgz", + "integrity": "sha512-/62tYiOe6DzS5BqVsNpH/nkGlX45C/Sp6V+NtiN6JQNS1Viay7cWkazmRkrQrdFj2eshDe96SIQNIoMxqhzBOg==" + }, + "remove-trailing-spaces": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/remove-trailing-spaces/-/remove-trailing-spaces-1.0.8.tgz", + "integrity": "sha512-O3vsMYfWighyFbTd8hk8VaSj9UAGENxAtX+//ugIst2RMk5e03h6RoIS+0ylsFxY1gvmPuAY/PO4It+gPEeySA==" + }, "require-directory": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", diff --git a/package.json b/package.json index c9aef901f..5158a6e45 100644 --- a/package.json +++ b/package.json @@ -30,6 +30,7 @@ "eslint-plugin-promise": "^6.0.0", "eslint-plugin-vue": "^9.3.0", "histoire": "^0.12.4", + "json-to-pretty-yaml": "^1.2.2", "markty-toml": "^0.1.1", "prettier": "^2.5.1", "prettier-plugin-toml": "^0.3.1", diff --git a/server/http.go b/server/http.go index 685b8da5a..fc631d8b3 100644 --- a/server/http.go +++ b/server/http.go @@ -97,6 +97,8 @@ func (s *HTTPd) RegisterSiteHandlers(site site.API, cache *util.Cache) { routes := map[string]route{ "health": {[]string{"GET"}, "/health", healthHandler(site)}, "state": {[]string{"GET"}, "/state", stateHandler(cache)}, + "config": {[]string{"GET"}, "/config/templates/{class:[a-z]+}", templatesHandler}, + "test": {[]string{"POST"}, "/config/test/{class:[a-z]+}", testHandler}, "buffersoc": {[]string{"POST", "OPTIONS"}, "/buffersoc/{value:[0-9.]+}", floatHandler(site.SetBufferSoc, site.GetBufferSoc)}, "prioritysoc": {[]string{"POST", "OPTIONS"}, "/prioritysoc/{value:[0-9.]+}", floatHandler(site.SetPrioritySoc, site.GetPrioritySoc)}, "residualpower": {[]string{"POST", "OPTIONS"}, "/residualpower/{value:[-0-9.]+}", floatHandler(site.SetResidualPower, site.GetResidualPower)}, diff --git a/server/http_config_handler.go b/server/http_config_handler.go new file mode 100644 index 000000000..898fd961a --- /dev/null +++ b/server/http_config_handler.go @@ -0,0 +1,118 @@ +package server + +import ( + "encoding/json" + "net/http" + + "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" + "github.com/gorilla/mux" +) + +// templatesHandler returns the list of templates by class +func templatesHandler(w http.ResponseWriter, r *http.Request) { + vars := mux.Vars(r) + + class, err := templates.ClassString(vars["class"]) + if err != nil { + jsonError(w, http.StatusBadRequest, err) + return + } + + res := templates.ByClass(class) + jsonResult(w, res) +} + +// testHandler tests a configuration by class +func testHandler(w http.ResponseWriter, r *http.Request) { + vars := mux.Vars(r) + + class, err := templates.ClassString(vars["class"]) + if err != nil { + jsonError(w, http.StatusBadRequest, err) + return + } + + var req map[string]any + if err := json.NewDecoder(r.Body).Decode(&req); err != nil { + jsonError(w, http.StatusBadRequest, err) + return + } + + // var req struct { + // Type string + // Other map[string]any `mapstructure:",remain"` + // } + + // if err := mapstructure.Decode(plain, &req); err != nil { + // jsonError(w, http.StatusBadRequest, err) + // return + // } + + // tmpl, err := templates.ByName(class, req.Name) + // if err != nil { + // jsonError(w, http.StatusBadRequest, err) + // return + // } + + // b, _, err := tmpl.RenderResult(templates.TemplateRenderModeInstance, req.Other) + // if err != nil { + // jsonError(w, http.StatusBadRequest, err) + // return + // } + + // var instance any + // if err := yaml.Unmarshal(b, &instance); err != nil { + // jsonError(w, http.StatusBadRequest, err) + // return + // } + + typ := "template" + if req["type"] != nil { + typ = req["type"].(string) + delete(req, "type") + } + + var dev any + + switch class { + case templates.Charger: + dev, err = charger.NewFromConfig(typ, req) + case templates.Meter: + dev, err = meter.NewFromConfig(typ, req) + case templates.Vehicle: + dev, err = vehicle.NewFromConfig(typ, req) + } + + if err != nil { + jsonError(w, http.StatusBadRequest, err) + return + } + + type result = struct { + Value any `json:"value"` + Error error `json:"error"` + } + + res := make(map[string]result) + + if dev, ok := dev.(api.Meter); ok { + val, err := dev.CurrentPower() + res["CurrentPower"] = result{val, err} + } + + if dev, ok := dev.(api.MeterEnergy); ok { + val, err := dev.TotalEnergy() + res["TotalEnergy"] = result{val, err} + } + + if dev, ok := dev.(api.Battery); ok { + val, err := dev.Soc() + res["Soc"] = result{val, err} + } + + jsonResult(w, res) +} diff --git a/templates/definition/charger/homematic.yaml b/templates/definition/charger/homematic.yaml index 6626533ca..a21a87727 100644 --- a/templates/definition/charger/homematic.yaml +++ b/templates/definition/charger/homematic.yaml @@ -28,7 +28,7 @@ params: mask: true - name: meterchannel default: 6 - valuetype: int + type: int required: true advanced: true description: @@ -39,7 +39,7 @@ params: de: Kanalnummer des Messwertkanals, wie im CCU Webfrontend angezeigt. - name: switchchannel default: 3 - valuetype: int + type: int required: true advanced: true description: diff --git a/templates/definition/charger/ocpp.yaml b/templates/definition/charger/ocpp.yaml index fd33c00b9..26b7807bd 100644 --- a/templates/definition/charger/ocpp.yaml +++ b/templates/definition/charger/ocpp.yaml @@ -36,7 +36,7 @@ requirements: * Local network connection params: - name: stationid - valuetype: string + type: string advanced: true help: en: The chargers unique station id (or chargepoint id) @@ -44,19 +44,19 @@ params: example: EVB-P12354 - name: connector default: 1 - valuetype: int + type: int advanced: true help: en: Connector number, usually 1 for first connector. de: Verwendeter Ladepunkt, normalerweise 1 für den ersten Anschluss. - name: meter default: false - valuetype: bool + type: bool help: en: Use integrated meter (if present) de: Benutze den integrierten Zähler (falls vorhanden) - name: idtag - valuetype: string + type: string advanced: true help: en: Token-ID returned to the charger for authorisation of charging sessions @@ -65,7 +65,7 @@ params: - name: timeout advanced: true default: 10m - valuetype: duration + type: duration render: | type: ocpp {{- if ne .stationid "" }} diff --git a/templates/definition/charger/openwb.yaml b/templates/definition/charger/openwb.yaml index 58b106a3b..95b36f898 100644 --- a/templates/definition/charger/openwb.yaml +++ b/templates/definition/charger/openwb.yaml @@ -12,7 +12,7 @@ params: - name: host - name: connector - name: phases1p3p - valuetype: bool + type: bool description: en: Charger is equipped with phase switching feature de: Phasenumschaltung vorhanden diff --git a/templates/definition/charger/tinkerforge-warp.yaml b/templates/definition/charger/tinkerforge-warp.yaml index 3c902b57b..34a4e7828 100644 --- a/templates/definition/charger/tinkerforge-warp.yaml +++ b/templates/definition/charger/tinkerforge-warp.yaml @@ -17,7 +17,7 @@ requirements: params: - name: fw2 required: true - valuetype: bool + type: bool help: de: Firmware v2 installiert en: Firmware v2 installed @@ -39,7 +39,7 @@ params: - name: timeout advanced: true default: 30s - valuetype: duration + type: duration help: de: Akzeptiere keine Daten die älter sind als dieser Wert en: Don't accept values older than this value diff --git a/templates/definition/defaults.yaml b/templates/definition/defaults.yaml index 190441cd2..05f55986f 100644 --- a/templates/definition/defaults.yaml +++ b/templates/definition/defaults.yaml @@ -10,7 +10,7 @@ params: description: generic: Modbus ID default: 1 - valuetype: number + type: number - name: modbusdevice description: de: Geräteadresse @@ -27,7 +27,7 @@ params: de: Prüfe die Geräteeinstellungen, typische Werte sind 9600, 19200, 38400, 57600, 115200 en: Check the device settings, typical values are 9600, 19200, 38400, 57600, 115200 default: 9600 - valuetype: number + type: number - name: modbuscomset description: de: ComSet @@ -60,7 +60,7 @@ params: help: de: Port en: Port - valuetype: number + type: number - name: interface description: de: Netzwerkinterface @@ -100,7 +100,7 @@ params: de: Akkukapazität in kWh en: Battery capacity in kWh example: "50" - valuetype: float + type: float usages: ["vehicle", "battery"] - name: vin description: @@ -118,7 +118,7 @@ params: de: Die maximale Anzahl der Phasen welche genutzt werden können en: The maximum number of phases which can be used example: 3 - valuetype: number + type: number - name: connector description: de: Ladepunkt (falls >1 Ladepunkt) @@ -133,7 +133,7 @@ params: de: Zeitintervall nach dem Daten erneut vom Fahrzeug abgefragt werden en: Time interval with when data should be reloaded from the vehicle advanced: true - valuetype: duration + type: duration example: 5m - name: cloud description: @@ -158,14 +158,14 @@ params: - No waiting for a new evcc version in this case and then requiring an update requirements: evcc: ["sponsorship"] - valuetype: bool + type: bool - name: timeout description: de: Zeitüberschreitung en: Timeout advanced: true example: 10s - valuetype: duration + type: duration - name: mode description: de: Standard-Lademodus, wenn ein Fahrzeug angeschlossen ist @@ -173,7 +173,7 @@ params: help: de: Möglich sind Off, Now, MinPV und PV, oder leer wenn keiner definiert werden soll en: Possible values are Off, Now, MinPV and PV, or empty if none should be set - valuetype: chargemodes + type: chargemodes - name: minsoc description: de: Minimaler Ladestand (Soc) in % @@ -182,7 +182,7 @@ params: de: Lade sofort mit maximaler Geschwindigkeit bis zu dem angegeben Ladestand, wenn der Lademodus nicht auf 'Aus' steht en: Charge immediately with maximum power up to the defined state of charge, if the charge mode is not set to 'OFF' example: 25 - valuetype: number + type: number - name: targetsoc description: de: Ziel-Ladestand (Soc) in % @@ -191,7 +191,7 @@ params: de: Bis zu welchem Ladestand (Soc) soll das Fahrzeug geladen werden en: Until which state of charge (Soc) should the vehicle be charged example: 80 - valuetype: number + type: number - name: mincurrent description: de: Minimale Stromstärke in Ampere (A) @@ -200,7 +200,7 @@ params: de: Definiert die minimale Stromstärke pro angeschlossener Phase mit welcher das Fahrzeug geladen werden soll en: The minimum amperage per connected phase with which the car should be charged example: 6 - valuetype: number + type: number - name: maxcurrent description: de: Maximale Stromstärke in Ampere (A) @@ -209,7 +209,7 @@ params: de: Definiert die maximale Stromstärke pro angeschlossener Phase mit welcher das Fahrzeug geladen werden soll en: The maximum amperage per connected phase with which the car shuold be charged example: 16 - valuetype: number + type: number - name: identifiers description: de: Identifikation @@ -217,7 +217,7 @@ params: help: de: "Kann meist erst später eingetragen werden, siehe: https://docs.evcc.io/docs/guides/vehicles/#erkennung-des-fahrzeugs-an-der-wallbox" en: "Mostly this can be added later, see: https://docs.evcc.io/docs/guides/vehicles/#erkennung-des-fahrzeugs-an-der-wallbox" - valuetype: stringlist + type: stringlist - name: standbypower description: de: Standby-Leistung in W @@ -225,7 +225,7 @@ params: help: de: Leistung oberhalb des angegebenen Wertes wird als Ladeleistung gewertet en: Power values above this value will be considered as charging pow - valuetype: number + type: number - name: language description: de: Sprache @@ -282,7 +282,7 @@ presets: - name: cache default: 15m advanced: true - valuetype: duration + type: duration render: | {{define "vehicle-base"}} {{- if ne .title "" }} @@ -320,7 +320,7 @@ presets: advanced: true - name: identifiers advanced: true - valuetype: stringlist + type: stringlist render: | {{define "vehicle-identify"}} {{- if or (ne .mode "") (ne .minSoc "") (ne .targetSoc "") (ne .minCurrent "") (ne .maxCurrent "") }} diff --git a/templates/definition/meter/dsmr.yaml b/templates/definition/meter/dsmr.yaml index 63202be18..ff60c547b 100644 --- a/templates/definition/meter/dsmr.yaml +++ b/templates/definition/meter/dsmr.yaml @@ -15,7 +15,7 @@ params: de: Typischerweise 1-0:1.8.0, bei Mehrtarifzählern 1-0:1.8.1 oder 1-0:1.8.2 en: Typically 1-0:1.8.0 or 1-0:1.8.1/1-0:1.8.2 with multiple tariffs advanced: true - valuetype: string + type: string render: | type: dsmr uri: {{ .host }}:{{ .port }} diff --git a/templates/definition/meter/homematic.yaml b/templates/definition/meter/homematic.yaml index ef3beedf5..edaf1b1ee 100644 --- a/templates/definition/meter/homematic.yaml +++ b/templates/definition/meter/homematic.yaml @@ -23,7 +23,7 @@ params: mask: true - name: meterchannel default: 6 - valuetype: int + type: int required: true advanced: true description: diff --git a/templates/definition/meter/huawei-sun2000-dongle-powersensor.yaml b/templates/definition/meter/huawei-sun2000-dongle-powersensor.yaml index 9bc35d7c2..430ee3cf2 100644 --- a/templates/definition/meter/huawei-sun2000-dongle-powersensor.yaml +++ b/templates/definition/meter/huawei-sun2000-dongle-powersensor.yaml @@ -8,7 +8,7 @@ params: choice: ["grid", "pv", "battery"] allinone: true - name: storageunit - valuetype: number + type: number default: 1 advanced: true - name: modbus diff --git a/templates/definition/meter/solaredge-hybrid.yaml b/templates/definition/meter/solaredge-hybrid.yaml index d607aa362..8f831e260 100644 --- a/templates/definition/meter/solaredge-hybrid.yaml +++ b/templates/definition/meter/solaredge-hybrid.yaml @@ -16,7 +16,7 @@ params: id: 1 port: 1502 - name: timeout - valuetype: duration + type: duration - name: capacity advanced: true render: | diff --git a/templates/definition/meter/solaredge-inverter.yaml b/templates/definition/meter/solaredge-inverter.yaml index cf9c158b5..0fb6abf76 100644 --- a/templates/definition/meter/solaredge-inverter.yaml +++ b/templates/definition/meter/solaredge-inverter.yaml @@ -17,7 +17,7 @@ params: id: 1 port: 1502 - name: timeout - valuetype: duration + type: duration render: | type: custom power: diff --git a/templates/definition/meter/sunspec-hybrid.yaml b/templates/definition/meter/sunspec-hybrid.yaml index d37aeba80..78ca3ea33 100644 --- a/templates/definition/meter/sunspec-hybrid.yaml +++ b/templates/definition/meter/sunspec-hybrid.yaml @@ -19,7 +19,7 @@ params: de: Einstellung Float/Integer im Wechselrichter überprüfen en: Verify Float/Integer setting in inverter advanced: true - valuetype: bool + type: bool - name: capacity advanced: true render: | diff --git a/templates/definition/meter/sunspec-inverter.yaml b/templates/definition/meter/sunspec-inverter.yaml index 76ce7ba2f..c86d31fdc 100644 --- a/templates/definition/meter/sunspec-inverter.yaml +++ b/templates/definition/meter/sunspec-inverter.yaml @@ -18,7 +18,7 @@ params: de: Einstellung Float/Integer im Wechselrichter überprüfen en: Verify Float/Integer setting in inverter advanced: true - valuetype: bool + type: bool render: | type: modbus {{- include "modbus" . }} diff --git a/templates/definition/vehicle/evnotify.yaml b/templates/definition/vehicle/evnotify.yaml index 5382cda75..e731af8fe 100644 --- a/templates/definition/vehicle/evnotify.yaml +++ b/templates/definition/vehicle/evnotify.yaml @@ -10,7 +10,7 @@ params: - name: token required: true - name: capacity - valuetype: float + type: float - name: phases advanced: true - name: icon diff --git a/templates/definition/vehicle/offline.yaml b/templates/definition/vehicle/offline.yaml index 6bba62a0a..2ed541f59 100644 --- a/templates/definition/vehicle/offline.yaml +++ b/templates/definition/vehicle/offline.yaml @@ -19,7 +19,7 @@ params: advanced: true - name: identifiers advanced: true - valuetype: stringlist + type: stringlist render: | type: custom features: ["offline"] diff --git a/util/templates/class.go b/util/templates/class.go new file mode 100644 index 000000000..d88ee114a --- /dev/null +++ b/util/templates/class.go @@ -0,0 +1,19 @@ +package templates + +type Class int + +func (c *Class) UnmarshalText(text []byte) error { + class, err := ClassString(string(text)) + if err == nil { + *c = class + } + return err +} + +//go:generate enumer -type Class +const ( + _ Class = iota + Charger + Meter + Vehicle +) diff --git a/util/templates/class_enumer.go b/util/templates/class_enumer.go new file mode 100644 index 000000000..bea859f01 --- /dev/null +++ b/util/templates/class_enumer.go @@ -0,0 +1,83 @@ +// Code generated by "enumer -type Class"; DO NOT EDIT. + +package templates + +import ( + "fmt" + "strings" +) + +const _ClassName = "ChargerMeterVehicle" + +var _ClassIndex = [...]uint8{0, 7, 12, 19} + +const _ClassLowerName = "chargermetervehicle" + +func (i Class) String() string { + i -= 1 + if i < 0 || i >= Class(len(_ClassIndex)-1) { + return fmt.Sprintf("Class(%d)", i+1) + } + return _ClassName[_ClassIndex[i]:_ClassIndex[i+1]] +} + +// An "invalid array index" compiler error signifies that the constant values have changed. +// Re-run the stringer command to generate them again. +func _ClassNoOp() { + var x [1]struct{} + _ = x[Charger-(1)] + _ = x[Meter-(2)] + _ = x[Vehicle-(3)] +} + +var _ClassValues = []Class{Charger, Meter, Vehicle} + +var _ClassNameToValueMap = map[string]Class{ + _ClassName[0:7]: Charger, + _ClassLowerName[0:7]: Charger, + _ClassName[7:12]: Meter, + _ClassLowerName[7:12]: Meter, + _ClassName[12:19]: Vehicle, + _ClassLowerName[12:19]: Vehicle, +} + +var _ClassNames = []string{ + _ClassName[0:7], + _ClassName[7:12], + _ClassName[12:19], +} + +// ClassString retrieves an enum value from the enum constants string name. +// Throws an error if the param is not part of the enum. +func ClassString(s string) (Class, error) { + if val, ok := _ClassNameToValueMap[s]; ok { + return val, nil + } + + if val, ok := _ClassNameToValueMap[strings.ToLower(s)]; ok { + return val, nil + } + return 0, fmt.Errorf("%s does not belong to Class values", s) +} + +// ClassValues returns all values of the enum +func ClassValues() []Class { + return _ClassValues +} + +// ClassStrings returns a slice of all String values of the enum +func ClassStrings() []string { + strs := make([]string, len(_ClassNames)) + copy(strs, _ClassNames) + return strs +} + +// IsAClass returns "true" if the value is listed in the enum definition. "false" otherwise +func (i Class) IsAClass() bool { + for _, v := range _ClassValues { + if i == v { + return true + } + } + return false +} diff --git a/util/templates/config.go b/util/templates/config.go index 455b7281f..0d8bb7b43 100644 --- a/util/templates/config.go +++ b/util/templates/config.go @@ -41,8 +41,8 @@ func (c *ConfigDefaults) LoadDefaults() { if p.IsReference() { finalName := p.Name referencedItemName := p.Name - if p.Referencename != "" { - referencedItemName = p.Referencename + if p.ReferenceName != "" { + referencedItemName = p.ReferenceName } _, referencedParam := c.ParamByName(referencedItemName) referencedParam.OverwriteProperties(p) diff --git a/util/templates/generate/generate.go b/util/templates/generate/generate.go index 69aceaf68..f6936b45b 100644 --- a/util/templates/generate/generate.go +++ b/util/templates/generate/generate.go @@ -65,7 +65,7 @@ func writeTemplate(class templates.Class, index int, product templates.Product, b, err := tmpl.RenderDocumentation(product, values, "de") if err == nil { - filename := fmt.Sprintf("%s/%s/%s_%d.yaml", docsPath, class, tmpl.Template, index) + filename := fmt.Sprintf("%s/%s/%s_%d.yaml", docsPath, strings.ToLower(class.String()), tmpl.Template, index) err = os.WriteFile(filename, b, 0o644) } diff --git a/util/templates/init.go b/util/templates/init.go index 2ad93feb1..8a0fac2d5 100644 --- a/util/templates/init.go +++ b/util/templates/init.go @@ -16,14 +16,6 @@ var ( configDefaults = ConfigDefaults{} ) -type Class string - -const ( - Charger Class = "charger" - Meter Class = "meter" - Vehicle Class = "vehicle" -) - func init() { configDefaults.LoadDefaults() @@ -58,6 +50,13 @@ func FromBytes(b []byte) (Template, error) { err = tmpl.Validate() } + // set default value type + for _, p := range tmpl.Params { + if p.Type == "" { + p.Type = ParamTypeString + } + } + return tmpl, err } @@ -84,8 +83,12 @@ func loadTemplates(class Class) { return fmt.Errorf("processing template '%s' failed: %w", filepath, err) } - path := Class(path.Dir(filepath)) - templates[path] = append(templates[path], tmpl) + class, err := ClassString(path.Dir(filepath)) + if err != nil { + return fmt.Errorf("invalid template class: '%s'", err) + } + + templates[class] = append(templates[class], tmpl) return nil }) diff --git a/util/templates/template.go b/util/templates/template.go index 607f75a21..cae4f05c1 100644 --- a/util/templates/template.go +++ b/util/templates/template.go @@ -16,7 +16,7 @@ import ( type Template struct { TemplateDefinition - ConfigDefaults ConfigDefaults + ConfigDefaults ConfigDefaults `json:"-"` Lang string @@ -33,8 +33,8 @@ func (t *Template) GuidedSetupEnabled() bool { // UpdateParamWithDefaults adds default values to specific param name entries func (t *Template) UpdateParamsWithDefaults() error { for i, p := range t.Params { - if p.ValueType == "" || (p.ValueType != "" && !slices.Contains(ValidParamValueTypes, p.ValueType)) { - t.Params[i].ValueType = ParamValueTypeString + if p.Type == "" || (p.Type != "" && !slices.Contains(ValidParamTypes, p.Type)) { + t.Params[i].Type = ParamTypeString } if index, resultMapItem := t.ConfigDefaults.ParamByName(strings.ToLower(p.Name)); index > -1 { @@ -75,8 +75,8 @@ func (t *Template) Validate() error { } } - if p.ValueType != "" && !slices.Contains(ValidParamValueTypes, p.ValueType) { - return fmt.Errorf("invalid value type '%s' in template %s", p.ValueType, t.Template) + if p.Type != "" && !slices.Contains(ValidParamTypes, p.Type) { + return fmt.Errorf("invalid value type '%s' in template %s", p.Type, t.Template) } } @@ -234,8 +234,8 @@ func (t *Template) RenderProxyWithValues(values map[string]interface{}, lang str continue } - switch p.ValueType { - case ParamValueTypeStringList: + switch p.Type { + case ParamTypeStringList: for _, e := range v.([]string) { t.Params[index].Values = append(p.Values, yamlQuote(e)) } @@ -254,8 +254,8 @@ func (t *Template) RenderProxyWithValues(values map[string]interface{}, lang str var newParams []Param for _, param := range t.Params { if !param.IsRequired() { - switch param.ValueType { - case ParamValueTypeStringList: + switch param.Type { + case ParamTypeStringList: if len(param.Values) == 0 { continue } diff --git a/util/templates/template_documentation.go b/util/templates/template_documentation.go index 572b5f4c3..5996dd0ae 100644 --- a/util/templates/template_documentation.go +++ b/util/templates/template_documentation.go @@ -24,8 +24,8 @@ func (t *Template) RenderDocumentation(product Product, values map[string]interf continue } - switch p.ValueType { - case ParamValueTypeStringList: + switch p.Type { + case ParamTypeStringList: for _, e := range v.([]string) { t.Params[index].Values = append(p.Values, yamlQuote(e)) } diff --git a/util/templates/template_types.go b/util/templates/template_types.go index db8f05df9..9df7b426c 100644 --- a/util/templates/template_types.go +++ b/util/templates/template_types.go @@ -40,16 +40,16 @@ const ( ) const ( - ParamValueTypeString = "string" - ParamValueTypeNumber = "number" - ParamValueTypeFloat = "float" - ParamValueTypeBool = "bool" - ParamValueTypeStringList = "stringlist" - ParamValueTypeChargeModes = "chargemodes" - ParamValueTypeDuration = "duration" + ParamTypeString = "string" + ParamTypeNumber = "number" + ParamTypeFloat = "float" + ParamTypeBool = "bool" + ParamTypeStringList = "stringlist" + ParamTypeChargeModes = "chargemodes" + ParamTypeDuration = "duration" ) -var ValidParamValueTypes = []string{ParamValueTypeString, ParamValueTypeNumber, ParamValueTypeFloat, ParamValueTypeBool, ParamValueTypeStringList, ParamValueTypeChargeModes, ParamValueTypeDuration} +var ValidParamTypes = []string{ParamTypeString, ParamTypeNumber, ParamTypeFloat, ParamTypeBool, ParamTypeStringList, ParamTypeChargeModes, ParamTypeDuration} var ( ValidModbusChoices = []string{ModbusChoiceRS485, ModbusChoiceTCPIP} @@ -91,9 +91,9 @@ var predefinedTemplateProperties = []string{ // TextLanguage contains language-specific texts type TextLanguage struct { - Generic string // language independent - DE string // german text - EN string // english text + Generic string `json:",omitempty"` // language independent + DE string `json:",omitempty"` // german text + EN string `json:",omitempty"` // english text } func (t *TextLanguage) String(lang string) string { @@ -166,44 +166,45 @@ type LinkedTemplate struct { // 2. in defaults.yaml presets: can ne referenced in 1 and some values set here can be overwritten in 1. See OverwriteProperties method // 3. in defaults.yaml modbus section: are referenced in 1 by a `name:modbus` param entry. Some values here can be overwritten in 1. // 4. in defaults.yaml param section: defaults for some params -// Generelle Reihenfolge der Werte (außer Description, Default, ValueType): +// Generelle Reihenfolge der Werte (außer Description, Default, Type): // 1. defaults.yaml param section // 2. defaults.yaml presets // 3. defaults.yaml modbus section // 4. template type Param struct { - Reference *bool // if this is references another param definition - Referencename string // name of the referenced param if it is not identical to the defined name - Preset string // Reference a predefined set of params Name string // Param name which is used for assigning defaults properties and referencing in render Description TextLanguage // language specific titles (presented in UI instead of Name) - Required *bool // cli if the user has to provide a non empty value - Mask *bool // cli if the value should be masked, e.g. for passwords - Advanced *bool // cli if the user does not need to be asked. Requires a "Default" to be defined. - Hidden *bool // cli if the parameter should not be presented in the cli, the default value be assigned - Deprecated *bool // if the parameter is deprecated and thus should not be presented in the cli or docs - Default string // default value if no user value is provided in the configuration - Example string // cli example value Help TextLanguage // cli configuration help - Value string // user provided value via cli configuration - Values []string // user provided list of values e.g. for ValueType "stringlist" - Usages []string // restrict param to these usage types, e.g. "battery" for home battery capacity - ValueType string // string representation of the value type, "string" is default - ValidValues []string // list of valid values the user can provide - Choice []string // defines a set of choices, e.g. "grid", "pv", "battery", "charge" for "usage" - AllInOne *bool // defines if the defined usages can all be present in a single device - Requirements Requirements // requirements for this param to be usable, only supported via ValueType "bool" + Reference *bool `json:",omitempty"` // if this is references another param definition + ReferenceName string `json:",omitempty"` // name of the referenced param if it is not identical to the defined name + Preset string `json:"-"` // Reference a predefined set of params + Required *bool `json:",omitempty"` // cli if the user has to provide a non empty value + Mask *bool `json:",omitempty"` // cli if the value should be masked, e.g. for passwords + Advanced *bool `json:",omitempty"` // cli if the user does not need to be asked. Requires a "Default" to be defined. + Hidden *bool `json:",omitempty"` // cli if the parameter should not be presented in the cli, the default value be assigned + Deprecated *bool `json:",omitempty"` // if the parameter is deprecated and thus should not be presented in the cli or docs + Default string `json:",omitempty"` // default value if no user value is provided in the configuration + Example string `json:",omitempty"` // cli example value + Value string `json:"-"` // user provided value via cli configuration + Values []string `json:",omitempty"` // user provided list of values e.g. for Type "stringlist" + Usages []string `json:",omitempty"` // restrict param to these usage types, e.g. "battery" for home battery capacity + Type string // string representation of the value type, "string" is default + ValidValues []string `json:",omitempty"` // list of valid values the user can provide + Choice []string `json:",omitempty"` // defines a set of choices, e.g. "grid", "pv", "battery", "charge" for "usage" + AllInOne *bool `json:"-"` // defines if the defined usages can all be present in a single device + Requirements Requirements `json:"-"` // requirements for this param to be usable, only supported via Type "bool" - Baudrate int // device specific default for modbus RS485 baudrate - Comset string // device specific default for modbus RS485 comset - Port int // device specific default for modbus TCPIP port - ID int // device specific default for modbus ID + // TODO move somewhere else should not be part of the param definition + Baudrate int `json:"-"` // device specific default for modbus RS485 baudrate + Comset string `json:"-"` // device specific default for modbus RS485 comset + Port int `json:"-"` // device specific default for modbus TCPIP port + ID int `json:"-"` // device specific default for modbus ID } // return a default value or example value depending on the renderMode func (p *Param) DefaultValue(renderMode string) interface{} { // return empty list to allow iterating over in template - if p.ValueType == ParamValueTypeStringList { + if p.Type == ParamTypeStringList { return []string{} } @@ -252,7 +253,7 @@ func (p *Param) IsAllInOne() bool { // Product contains naming information about a product a template supports type Product struct { Brand string // product brand - Description TextLanguage // product name + Description TextLanguage `json:",omitempty"` // product name } func (p Product) Title(lang string) string { @@ -262,12 +263,12 @@ func (p Product) Title(lang string) string { // TemplateDefinition contains properties of a device template type TemplateDefinition struct { Template string - Group string // the group this template belongs to, references groupList entries - Covers []string // list of covered outdated template names - Products []Product // list of products this template is compatible with - Capabilities []string - Requirements Requirements - Linked []LinkedTemplate // a list of templates that should be processed as part of the guided setup - Params []Param - Render string // rendering template + 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 []string `json:",omitempty"` + Requirements Requirements `json:"-"` + Linked []LinkedTemplate `json:",omitempty"` // a list of templates that should be processed as part of the guided setup + Params []Param `json:",omitempty"` + Render string `json:"-"` // rendering template }