Add configuration api (#6035)
Co-authored-by: Michael Geers <michael@geers.tv>
This commit is contained in:
parent
cfb4ef1c92
commit
54321c0cf7
42 changed files with 699 additions and 235 deletions
|
|
@ -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) {
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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: {
|
||||
|
|
|
|||
|
|
@ -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: {
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
198
assets/js/components/VehicleSettingsModal.vue
Normal file
198
assets/js/components/VehicleSettingsModal.vue
Normal 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>
|
||||
|
|
@ -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
27
assets/js/experimental.js
Normal 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;
|
||||
}
|
||||
|
|
@ -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;
|
||||
},
|
||||
};
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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())
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
||||
|
|
|
|||
|
|
@ -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))
|
||||
|
|
|
|||
13
i18n/en.toml
13
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"
|
||||
|
|
|
|||
45
package-lock.json
generated
45
package-lock.json
generated
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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)},
|
||||
|
|
|
|||
118
server/http_config_handler.go
Normal file
118
server/http_config_handler.go
Normal 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)
|
||||
}
|
||||
|
|
@ -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:
|
||||
|
|
|
|||
|
|
@ -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 "" }}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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 "") }}
|
||||
|
|
|
|||
|
|
@ -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 }}
|
||||
|
|
|
|||
|
|
@ -23,7 +23,7 @@ params:
|
|||
mask: true
|
||||
- name: meterchannel
|
||||
default: 6
|
||||
valuetype: int
|
||||
type: int
|
||||
required: true
|
||||
advanced: true
|
||||
description:
|
||||
|
|
|
|||
|
|
@ -8,7 +8,7 @@ params:
|
|||
choice: ["grid", "pv", "battery"]
|
||||
allinone: true
|
||||
- name: storageunit
|
||||
valuetype: number
|
||||
type: number
|
||||
default: 1
|
||||
advanced: true
|
||||
- name: modbus
|
||||
|
|
|
|||
|
|
@ -16,7 +16,7 @@ params:
|
|||
id: 1
|
||||
port: 1502
|
||||
- name: timeout
|
||||
valuetype: duration
|
||||
type: duration
|
||||
- name: capacity
|
||||
advanced: true
|
||||
render: |
|
||||
|
|
|
|||
|
|
@ -17,7 +17,7 @@ params:
|
|||
id: 1
|
||||
port: 1502
|
||||
- name: timeout
|
||||
valuetype: duration
|
||||
type: duration
|
||||
render: |
|
||||
type: custom
|
||||
power:
|
||||
|
|
|
|||
|
|
@ -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: |
|
||||
|
|
|
|||
|
|
@ -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" . }}
|
||||
|
|
|
|||
|
|
@ -10,7 +10,7 @@ params:
|
|||
- name: token
|
||||
required: true
|
||||
- name: capacity
|
||||
valuetype: float
|
||||
type: float
|
||||
- name: phases
|
||||
advanced: true
|
||||
- name: icon
|
||||
|
|
|
|||
|
|
@ -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
19
util/templates/class.go
Normal 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
|
||||
)
|
||||
83
util/templates/class_enumer.go
Normal file
83
util/templates/class_enumer.go
Normal 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
|
||||
}
|
||||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
})
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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))
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue