Add configuration api (#6035)

Co-authored-by: Michael Geers <michael@geers.tv>
This commit is contained in:
andig 2023-02-08 11:29:27 +01:00 • committed by GitHub
parent cfb4ef1c92
commit 54321c0cf7
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
42 changed files with 699 additions and 235 deletions

View file

@ -83,6 +83,26 @@
<FormRow id="telemetryEnabled" :label="$t('settings.telemetry.label')">
<TelemetrySettings :sponsor="sponsor" class="mt-1 mb-0" />
</FormRow>
<FormRow
v-if="isNightly"
id="hiddenFeaturesEnabled"
:label="`${$t('settings.hiddenFeatures.label')} 🧪`"
>
<div class="form-check form-switch my-1">
<input
id="hiddenFeaturesEnabled"
v-model="hiddenFeatures"
class="form-check-input"
type="checkbox"
role="switch"
/>
<div class="form-check-label">
<label for="telemetryEnabled">
{{ $t("settings.hiddenFeatures.value") }}
</label>
</div>
</div>
</FormRow>
</div>
</div>
</div>
@ -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) {

View file

@ -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);

View file

@ -152,7 +152,7 @@
</div>
</div>
<div v-if="$hiddenFeatures" class="mb-3 row">
<div v-if="$hiddenFeatures()" class="mb-3 row">
<label
:for="formId('maxcurrent')"
class="col-sm-4 col-form-label pt-0 pt-sm-2"
@ -178,7 +178,7 @@
</div>
</div>
<div v-if="$hiddenFeatures" class="mb-3 row">
<div v-if="$hiddenFeatures()" class="mb-3 row">
<label
:for="formId('mincurrent')"
class="col-sm-4 col-form-label pt-0 pt-sm-2"
@ -272,10 +272,10 @@ export default {
return [0, 1, 3].includes(this.phasesConfigured);
},
showCurrentSettings: function () {
return this.$hiddenFeatures;
return this.$hiddenFeatures();
},
showMinSocSettings: function () {
return this.$hiddenFeatures;
return this.$hiddenFeatures();
},
},
watch: {

View file

@ -18,7 +18,7 @@
:loadpoints="loadpoints"
:vehicles="vehicles"
/>
<Vehicles v-if="showParkingLot" />
<VehcileSettingsModal />
<Footer v-bind="footer"></Footer>
</div>
</div>
@ -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: {

View file

@ -54,9 +54,6 @@
@target-energy-updated="targetEnergyUpdated"
/>
</div>
<div v-if="$hiddenFeatures" class="d-flex justify-content-start">
<small>vor 5 Stunden</small>
</div>
</div>
</template>

View file

@ -23,6 +23,17 @@
{{ $t("main.vehicle.unknown") }}
</button>
</li>
<div v-if="$hiddenFeatures()">
<div class="dropdown-divider"></div>
<li>
<h6 class="dropdown-header">{{ $t("main.vehicle.moreActions") }}</h6>
</li>
<li>
<button type="button" class="dropdown-item" @click="addVehicle">
{{ $t("main.vehicle.addVehicle") }} 🧪
</button>
</li>
</div>
</ul>
</div>
</template>
@ -30,6 +41,7 @@
<script>
import "@h2d2/shopicons/es/filled/options";
import Dropdown from "bootstrap/js/dist/dropdown";
import Modal from "bootstrap/js/dist/modal";
export default {
name: "VehicleOptions",
@ -38,7 +50,7 @@ export default {
vehicles: Array,
isUnknown: Boolean,
},
emits: ["change-vehicle", "remove-vehicle"],
emits: ["change-vehicle", "remove-vehicle", "add-vehicle"],
computed: {
dropdownId() {
return `vehicleOptionsDropdown${this.id}`;
@ -57,6 +69,14 @@ export default {
removeVehicle() {
this.$emit("remove-vehicle");
},
addVehicle() {
this.$emit("remove-vehicle");
const modal = Modal.getOrCreateInstance(
document.getElementById("vehicleSettingsModal")
);
modal.show();
},
},
};
</script>

View file

@ -0,0 +1,198 @@
<template>
<Teleport to="body">
<div
id="vehicleSettingsModal"
ref="modal"
class="modal fade text-dark"
data-bs-backdrop="true"
tabindex="-1"
role="dialog"
aria-hidden="true"
>
<div class="modal-dialog modal-dialog-centered" role="document">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title">Add New Vehicle 🧪</h5>
<button
type="button"
class="btn-close"
data-bs-dismiss="modal"
aria-label="Close"
></button>
</div>
<div class="modal-body">
<div class="container">
<FormRow id="vehicleTemplate" :label="$t('vehicleSettings.template')">
<select
id="vehicleTemplate"
v-model="template"
class="form-select form-select-sm w-100"
>
<option
v-for="option in vehicleOptions"
:key="option.name"
:value="option.value"
>
{{ option.name }}
</option>
</select>
</FormRow>
<FormRow
v-for="param in templateParams"
:id="`vehicleParam${param.Name}`"
:key="param.Name"
:label="param.Description.EN || `[${param.Name}]`"
>
<input
:id="`vehicleParam${param.Name}`"
v-model="values[param.Name]"
:type="param.Mask ? 'password' : 'text'"
class="w-100 me-2"
:placeholder="param.Example"
:required="param.Required"
/>
</FormRow>
<div class="buttons d-flex justify-content-between mb-4">
<button
type="button"
class="btn btn-outline-secondary"
data-bs-dismiss="modal"
>
{{ $t("vehicleSettings.cancel") }}
</button>
<button type="submit" class="btn btn-primary" @click="test">
{{ $t("vehicleSettings.test") }}
</button>
</div>
<div class="card result">
<div class="card-body">
<pre><code>{{ configYaml }}</code></pre>
<code
v-if="testResult"
:class="testSuccess ? 'text-success' : 'text-danger'"
>
<hr />
{{ testResult }}
</code>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</Teleport>
</template>
<script>
import FormRow from "./FormRow.vue";
import api from "../api";
import YAML from "json-to-pretty-yaml";
export default {
name: "VehicleSettingsModal",
components: { FormRow },
data() {
return {
isModalVisible: false,
templates: [],
template: null,
values: {},
testResult: "",
testSuccess: false,
};
},
computed: {
vehicleOptions() {
const result = [];
this.templates.forEach((t) => {
t.Products.forEach((p) => {
const value = t.Template;
let name = this.productName(p);
result.push({ name, value });
});
});
result.sort((a, b) => a.name.localeCompare(b.name));
return result;
},
templateParams() {
const params = this.templates.find((t) => t.Template === this.template)?.Params || [];
return params.filter((p) => !p.Advanced);
},
configYaml() {
return YAML.stringify([
{
name: "my_vehicle",
...this.apiData,
},
]);
},
apiData() {
return {
template: this.template,
...this.values,
};
},
},
watch: {
isModalVisible(visible) {
if (visible) {
this.loadTemplates();
}
},
template() {
this.values = {};
},
},
mounted() {
this.$refs.modal.addEventListener("show.bs.modal", this.modalVisible);
this.$refs.modal.addEventListener("hide.bs.modal", this.modalInvisible);
},
unmounted() {
this.$refs.modal.removeEventListener("show.bs.modal", this.modalVisible);
this.$refs.modal.removeEventListener("hide.bs.modal", this.modalInvisible);
},
methods: {
productName({ Brand, Description }) {
const brand = Brand || "";
const description = Description.Generic || Description.EN || "";
return `${brand} ${description}`.trim();
},
async loadTemplates() {
try {
this.templates = (await api.get("config/templates/vehicle")).data.result;
} catch (e) {
console.error(e);
}
},
async test() {
try {
this.testResult = (await api.post("config/test/vehicle", this.apiData)).data.result;
this.testSuccess = true;
} catch (e) {
console.error(e);
this.testSuccess = false;
this.testResult = e.response?.data?.error || e.message;
}
},
modalVisible() {
this.isModalVisible = true;
},
modalInvisible() {
this.isModalVisible = false;
},
},
};
</script>
<style scoped>
.container {
margin-left: calc(var(--bs-gutter-x) * -0.5);
margin-right: calc(var(--bs-gutter-x) * -0.5);
padding-right: 0;
}
.buttons,
.result {
margin-right: calc(var(--bs-gutter-x) * -0.5);
}
</style>

View file

@ -1,102 +0,0 @@
<template>
<div>
<div class="container px-4 mb-3 mb-sm-4 d-flex justify-content-start align-items-center">
<h2 class="m-0">{{ $t("main.vehicles") }}</h2>
<button
class="btn btn-link d-flex evcc-default-text p-2 ms-1 refresh"
:class="{
'refresh--in-progress': refreshing,
}"
:disabled="refreshing"
@click="refresh"
>
<shopicon-regular-refresh class="refresh-icon"></shopicon-regular-refresh>
</button>
</div>
<div class="container vehicles px-0 mb-5" :class="`vehicles-${vehicles.length}`">
<Vehicle
v-for="(vehicle, index) in vehicles"
v-bind="vehicle"
:id="`vehicle_${index}`"
:key="index"
class="vehicle"
parked
/>
</div>
</div>
</template>
<script>
import Vehicle from "./Vehicle.vue";
import "@h2d2/shopicons/es/regular/refresh";
export default {
name: "Vehicles",
components: { Vehicle },
data() {
return {
refreshing: false,
// TODO: mock data for development
vehicles: [
{
vehiclePresent: true,
vehicleTitle: "Ford Mustang Mach-E",
vehicleSoc: 46,
vehicleRange: 182,
targetSoc: 100,
},
{
vehiclePresent: true,
vehicleTitle: "Renault Twingo Electric",
vehicleSoc: 77,
vehicleRange: 98,
targetSoc: 90,
},
{
vehiclePresent: true,
vehicleTitle: "Blauer VW ID.4",
vehicleSoc: 16,
vehicleRange: 52,
minSoc: 35,
targetSoc: 60,
},
],
};
},
methods: {
refresh() {
this.refreshing = true;
// TODO: insert real implementation here
window.setTimeout(() => {
this.refreshing = false;
}, 5000);
},
},
};
</script>
<style scoped>
.vehicles {
display: grid;
grid-gap: 2rem;
grid-template-columns: repeat(auto-fit, minmax(310px, 1fr));
}
.vehicle {
border: 4px solid white;
}
.refresh--in-progress {
animation: rotation 1s infinite cubic-bezier(0.37, 0, 0.63, 1);
}
.refresh-icon {
transform: translateY(-2px);
}
@keyframes rotation {
from {
transform: rotate(0deg);
}
to {
transform: rotate(360deg);
}
}
</style>

27
assets/js/experimental.js Normal file
View file

@ -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;
}

View file

@ -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;
},
};

View file

@ -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;

View file

@ -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())
}
}

View file

@ -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"
}

View file

@ -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)

View file

@ -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))

View file

@ -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"

45
package-lock.json generated
View file

@ -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",

View file

@ -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",

View file

@ -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)},

View file

@ -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)
}

View file

@ -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:

View file

@ -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 "" }}

View file

@ -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

View file

@ -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

View file

@ -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 "") }}

View file

@ -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 }}

View file

@ -23,7 +23,7 @@ params:
mask: true
- name: meterchannel
default: 6
valuetype: int
type: int
required: true
advanced: true
description:

View file

@ -8,7 +8,7 @@ params:
choice: ["grid", "pv", "battery"]
allinone: true
- name: storageunit
valuetype: number
type: number
default: 1
advanced: true
- name: modbus

View file

@ -16,7 +16,7 @@ params:
id: 1
port: 1502
- name: timeout
valuetype: duration
type: duration
- name: capacity
advanced: true
render: |

View file

@ -17,7 +17,7 @@ params:
id: 1
port: 1502
- name: timeout
valuetype: duration
type: duration
render: |
type: custom
power:

View file

@ -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: |

View file

@ -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" . }}

View file

@ -10,7 +10,7 @@ params:
- name: token
required: true
- name: capacity
valuetype: float
type: float
- name: phases
advanced: true
- name: icon

View file

@ -19,7 +19,7 @@ params:
advanced: true
- name: identifiers
advanced: true
valuetype: stringlist
type: stringlist
render: |
type: custom
features: ["offline"]

19
util/templates/class.go Normal file
View file

@ -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
)

View file

@ -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
}

View file

@ -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)

View file

@ -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)
}

View file

@ -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
})

View file

@ -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
}

View file

@ -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))
}

View file

@ -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
}