513 lines
13 KiB
Vue
513 lines
13 KiB
Vue
<template>
|
|
<GenericModal
|
|
id="chargerModal"
|
|
ref="modal"
|
|
:title="modalTitle"
|
|
data-testid="charger-modal"
|
|
:fade="fade"
|
|
:size="modalSize"
|
|
@open="open"
|
|
@close="close"
|
|
>
|
|
<form ref="form" class="container mx-0 px-0">
|
|
<TemplateSelector
|
|
ref="templateSelect"
|
|
v-model="templateName"
|
|
device-type="charger"
|
|
:is-new="isNew"
|
|
:product-name="productName"
|
|
:groups="templateOptions"
|
|
@change="templateChanged"
|
|
/>
|
|
|
|
<YamlEntry
|
|
v-if="showYamlInput"
|
|
v-model="values.yaml"
|
|
type="charger"
|
|
:error-line="test.errorLine"
|
|
/>
|
|
<div v-else>
|
|
<p v-if="loadingTemplate">{{ $t("config.general.templateLoading") }}</p>
|
|
<SponsorTokenRequired v-if="sponsorTokenRequired" />
|
|
<Markdown v-if="description" :markdown="description" class="my-4" />
|
|
<FormRow
|
|
v-if="ocppUrl"
|
|
id="chargerOcppUrl"
|
|
:label="$t('config.charger.ocppLabel')"
|
|
:help="$t('config.charger.ocppHelp')"
|
|
>
|
|
<input type="text" class="form-control border" :value="ocppUrl" readonly />
|
|
</FormRow>
|
|
|
|
<Modbus
|
|
v-if="modbus"
|
|
v-model:modbus="values.modbus"
|
|
v-model:id="values.id"
|
|
v-model:host="values.host"
|
|
v-model:port="values.port"
|
|
v-model:device="values.device"
|
|
v-model:baudrate="values.baudrate"
|
|
v-model:comset="values.comset"
|
|
:defaultId="modbus.ID ? Number(modbus.ID) : undefined"
|
|
:defaultComset="modbus.Comset"
|
|
:defaultBaudrate="modbus.Baudrate"
|
|
:defaultPort="modbus.Port"
|
|
:capabilities="modbusCapabilities"
|
|
/>
|
|
<PropertyEntry
|
|
v-for="param in normalParams"
|
|
:id="`chargerParam${param.Name}`"
|
|
:key="param.Name"
|
|
v-bind="param"
|
|
v-model="values[param.Name]"
|
|
/>
|
|
|
|
<PropertyCollapsible>
|
|
<template v-if="advancedParams.length" #advanced>
|
|
<PropertyEntry
|
|
v-for="param in advancedParams"
|
|
:id="`chargerParam${param.Name}`"
|
|
:key="param.Name"
|
|
v-bind="param"
|
|
v-model="values[param.Name]"
|
|
/>
|
|
</template>
|
|
</PropertyCollapsible>
|
|
</div>
|
|
|
|
<DeviceModalActions
|
|
v-if="showActions"
|
|
:is-deletable="isDeletable"
|
|
:test-state="test"
|
|
:is-saving="saving"
|
|
:sponsor-token-required="sponsorTokenRequired"
|
|
@save="isNew ? create() : update()"
|
|
@remove="remove"
|
|
@test="testManually"
|
|
/>
|
|
</form>
|
|
</GenericModal>
|
|
</template>
|
|
|
|
<script lang="ts">
|
|
import { defineComponent } from "vue";
|
|
import FormRow from "./FormRow.vue";
|
|
import PropertyEntry from "./PropertyEntry.vue";
|
|
import PropertyCollapsible from "./PropertyCollapsible.vue";
|
|
import Modbus from "./DeviceModal/Modbus.vue";
|
|
import DeviceModalActions from "./DeviceModal/Actions.vue";
|
|
import GenericModal from "../Helper/GenericModal.vue";
|
|
import Markdown from "./Markdown.vue";
|
|
import SponsorTokenRequired from "./DeviceModal/SponsorTokenRequired.vue";
|
|
import TemplateSelector, { customTemplateOption } from "./DeviceModal/TemplateSelector.vue";
|
|
import YamlEntry from "./DeviceModal/YamlEntry.vue";
|
|
import { initialTestState, performTest } from "./utils/test";
|
|
import {
|
|
handleError,
|
|
ConfigType,
|
|
type DeviceValues,
|
|
type Template,
|
|
type Product,
|
|
type ModbusParam,
|
|
type ModbusCapability,
|
|
applyDefaultsFromTemplate,
|
|
createDeviceUtils,
|
|
customChargerName,
|
|
} from "./DeviceModal";
|
|
import customChargerYaml from "./defaultYaml/customCharger.yaml?raw";
|
|
import customHeaterYaml from "./defaultYaml/customHeater.yaml?raw";
|
|
import heatpumpYaml from "./defaultYaml/heatpump.yaml?raw";
|
|
import switchsocketHeaterYaml from "./defaultYaml/switchsocketHeater.yaml?raw";
|
|
import switchsocketChargerYaml from "./defaultYaml/switchsocketCharger.yaml?raw";
|
|
import sgreadyYaml from "./defaultYaml/sgready.yaml?raw";
|
|
import sgreadyBoostYaml from "./defaultYaml/sgreadyBoost.yaml?raw";
|
|
import { LOADPOINT_TYPE } from "@/types/evcc";
|
|
|
|
const initialValues = { type: ConfigType.Template };
|
|
const device = createDeviceUtils("charger");
|
|
|
|
function sleep(ms: number) {
|
|
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
}
|
|
|
|
const CUSTOM_FIELDS = ["modbus"];
|
|
|
|
type Requirements = {
|
|
Description?: string;
|
|
EVCC?: string[];
|
|
};
|
|
|
|
type ChargerDeviceValues = DeviceValues & {
|
|
modbus?: any;
|
|
id?: any;
|
|
host?: any;
|
|
port?: any;
|
|
device?: any;
|
|
baudrate?: any;
|
|
comset?: any;
|
|
yaml?: string;
|
|
};
|
|
|
|
export default defineComponent({
|
|
name: "ChargerModal",
|
|
components: {
|
|
FormRow,
|
|
PropertyEntry,
|
|
GenericModal,
|
|
Modbus,
|
|
PropertyCollapsible,
|
|
Markdown,
|
|
SponsorTokenRequired,
|
|
YamlEntry,
|
|
TemplateSelector,
|
|
DeviceModalActions,
|
|
},
|
|
props: {
|
|
id: Number,
|
|
name: String,
|
|
loadpointType: { type: String as () => LOADPOINT_TYPE | null, default: null },
|
|
fade: String,
|
|
isSponsor: Boolean,
|
|
},
|
|
emits: ["added", "updated", "removed", "close"],
|
|
data() {
|
|
return {
|
|
isModalVisible: false,
|
|
templates: [] as Template[],
|
|
products: [] as Product[],
|
|
templateName: null as string | null,
|
|
template: null as Template | null,
|
|
saving: false,
|
|
loadingTemplate: false,
|
|
values: { ...initialValues } as ChargerDeviceValues,
|
|
test: initialTestState(),
|
|
};
|
|
},
|
|
computed: {
|
|
modalTitle() {
|
|
if (this.isNew) {
|
|
return this.$t(`config.charger.titleAdd.${this.loadpointType}`);
|
|
}
|
|
return this.$t(`config.charger.titleEdit.${this.loadpointType}`);
|
|
},
|
|
modalSize() {
|
|
return this.showYamlInput ? "xl" : undefined;
|
|
},
|
|
templateOptions() {
|
|
const result = [];
|
|
|
|
if (this.isHeating) {
|
|
result.push({
|
|
label: "generic",
|
|
options: [
|
|
...this.products.filter((p) => p.group === "heatinggeneric"),
|
|
...[
|
|
ConfigType.Custom,
|
|
ConfigType.SgReadyBoost,
|
|
ConfigType.SgReady,
|
|
ConfigType.Heatpump,
|
|
ConfigType.SwitchSocket,
|
|
].map((type) =>
|
|
customTemplateOption(this.$t(customChargerName(type, true)), type)
|
|
),
|
|
],
|
|
});
|
|
result.push({
|
|
label: "heatingdevices",
|
|
options: this.products.filter((p) => p.group === "heating"),
|
|
});
|
|
} else {
|
|
result.push({
|
|
label: "generic",
|
|
options: [
|
|
...this.products.filter((p) => p.group === "generic"),
|
|
...[ConfigType.Custom, ConfigType.SwitchSocket].map((type) =>
|
|
customTemplateOption(this.$t(customChargerName(type, false)), type)
|
|
),
|
|
],
|
|
});
|
|
result.push({
|
|
label: "chargers",
|
|
options: this.products.filter((p) => !p.group),
|
|
});
|
|
}
|
|
|
|
result.push({
|
|
label: "switchsockets",
|
|
options: this.products.filter((p) => p.group === "switchsockets"),
|
|
});
|
|
|
|
return result;
|
|
},
|
|
templateParams() {
|
|
const params = this.template?.Params || [];
|
|
return params.filter(
|
|
(p) =>
|
|
!CUSTOM_FIELDS.includes(p.Name) &&
|
|
(p.Usages ? p.Usages.includes("charger") : true)
|
|
);
|
|
},
|
|
normalParams() {
|
|
return this.templateParams.filter((p) => !p.Advanced);
|
|
},
|
|
advancedParams() {
|
|
return this.templateParams.filter((p) => p.Advanced);
|
|
},
|
|
modbus() {
|
|
const params = this.template?.Params || [];
|
|
return (params as ModbusParam[]).find((p) => p.Name === "modbus");
|
|
},
|
|
modbusCapabilities() {
|
|
return (this.modbus?.Choice || []) as ModbusCapability[];
|
|
},
|
|
ocppUrl() {
|
|
const isOcpp =
|
|
this.templateParams.some((p) => p.Name === "connector") &&
|
|
this.templateParams.some((p) => p.Name === "stationid");
|
|
if (isOcpp) {
|
|
return `ws://${window.location.hostname}:8887/${this.values["stationid"] || ""}`;
|
|
}
|
|
return null;
|
|
},
|
|
modbusDefaults() {
|
|
const { ID, Comset, Baudrate, Port } = this.modbus || {};
|
|
return {
|
|
id: ID,
|
|
comset: Comset,
|
|
baudrate: Baudrate,
|
|
port: Port,
|
|
};
|
|
},
|
|
description() {
|
|
return this.template?.Requirements?.Description;
|
|
},
|
|
productName() {
|
|
return (
|
|
this.values.deviceProduct ||
|
|
this.templateName ||
|
|
this.$t(customChargerName(this.values.type, this.isHeating))
|
|
);
|
|
},
|
|
sponsorTokenRequired() {
|
|
const requirements = this.template?.Requirements as Requirements | undefined;
|
|
return requirements?.EVCC?.includes("sponsorship") && !this.isSponsor;
|
|
},
|
|
apiData() {
|
|
const data: Record<string, any> = {
|
|
...this.modbusDefaults,
|
|
...this.values,
|
|
};
|
|
if (this.values.type === ConfigType.Template) {
|
|
data["template"] = this.templateName;
|
|
}
|
|
return data;
|
|
},
|
|
isNew() {
|
|
return this.id === undefined;
|
|
},
|
|
isHeating() {
|
|
return this.loadpointType === LOADPOINT_TYPE.HEATING;
|
|
},
|
|
isDeletable() {
|
|
return !this.isNew;
|
|
},
|
|
showActions() {
|
|
return this.templateName || this.showYamlInput;
|
|
},
|
|
showYamlInput() {
|
|
return this.isYamlInput(this.values.type);
|
|
},
|
|
},
|
|
watch: {
|
|
isModalVisible(visible) {
|
|
if (visible) {
|
|
this.templateName = null;
|
|
this.reset();
|
|
this.test = initialTestState();
|
|
this.loadProducts();
|
|
if (this.id !== undefined) {
|
|
this.loadConfiguration();
|
|
}
|
|
}
|
|
},
|
|
templateName() {
|
|
this.loadTemplate();
|
|
},
|
|
values: {
|
|
handler() {
|
|
this.test = initialTestState();
|
|
},
|
|
deep: true,
|
|
},
|
|
},
|
|
methods: {
|
|
reset() {
|
|
this.values = { ...initialValues } as ChargerDeviceValues;
|
|
this.test = initialTestState();
|
|
},
|
|
async loadConfiguration() {
|
|
try {
|
|
const charger = await device.load(this.id as number);
|
|
this.values = charger.config;
|
|
// convert structure to flat list
|
|
// TODO: adjust GET response to match POST/PUT formats
|
|
this.values.type = charger.type;
|
|
this.values.deviceProduct = charger.deviceProduct;
|
|
this.applyDefaults();
|
|
this.templateName = this.values.template;
|
|
} catch (e) {
|
|
console.error(e);
|
|
}
|
|
},
|
|
applyDefaults() {
|
|
applyDefaultsFromTemplate(this.template, this.values);
|
|
if (this.isHeating) {
|
|
// enable heating and integrated device params if exist
|
|
const hasParam = (name: string) =>
|
|
this.template?.Params.some((p) => p.Name === name);
|
|
["heating", "integrateddevice"].forEach((param) => {
|
|
if (hasParam(param) && this.values[param] === undefined) {
|
|
this.values[param] = true;
|
|
}
|
|
});
|
|
// default heater icon
|
|
if (hasParam("icon") && this.values["icon"] === undefined) {
|
|
this.values["icon"] = "heater";
|
|
}
|
|
}
|
|
},
|
|
async loadProducts() {
|
|
if (!this.isModalVisible) {
|
|
return;
|
|
}
|
|
try {
|
|
this.products = await device.loadProducts(this.$i18n?.locale);
|
|
} catch (e) {
|
|
console.error(e);
|
|
}
|
|
},
|
|
async loadTemplate() {
|
|
this.template = null;
|
|
if (!this.templateName || this.isYamlInput(this.templateName as ConfigType)) return;
|
|
this.loadingTemplate = true;
|
|
try {
|
|
this.template = await device.loadTemplate(this.templateName, this.$i18n?.locale);
|
|
this.applyDefaults();
|
|
} catch (e) {
|
|
console.error(e);
|
|
}
|
|
this.loadingTemplate = false;
|
|
},
|
|
async create() {
|
|
// persist selected template product
|
|
if (this.template && this.$refs["templateSelect"]) {
|
|
this.values.deviceProduct = (this.$refs["templateSelect"] as any).getProductName();
|
|
}
|
|
|
|
if (this.test.isUnknown) {
|
|
const success = await performTest(
|
|
this.test,
|
|
this.testCharger,
|
|
this.$refs["form"] as HTMLFormElement
|
|
);
|
|
if (!success) return;
|
|
await sleep(100);
|
|
}
|
|
this.saving = true;
|
|
try {
|
|
const { name } = await device.create(this.apiData);
|
|
this.$emit("added", name);
|
|
(this.$refs["modal"] as any).close();
|
|
} catch (e) {
|
|
handleError(e, "create failed");
|
|
}
|
|
this.saving = false;
|
|
},
|
|
async testManually() {
|
|
await performTest(this.test, this.testCharger, this.$refs["form"] as HTMLFormElement);
|
|
},
|
|
async testCharger() {
|
|
return device.test(this.id, this.apiData);
|
|
},
|
|
async update() {
|
|
if (this.test.isUnknown) {
|
|
const success = await performTest(
|
|
this.test,
|
|
this.testCharger,
|
|
this.$refs["form"] as HTMLFormElement
|
|
);
|
|
if (!success) return;
|
|
await sleep(250);
|
|
}
|
|
this.saving = true;
|
|
try {
|
|
await device.update(this.id as number, this.apiData);
|
|
this.$emit("updated");
|
|
(this.$refs["modal"] as any).close();
|
|
} catch (e) {
|
|
handleError(e, "update failed");
|
|
}
|
|
this.saving = false;
|
|
},
|
|
async remove() {
|
|
try {
|
|
await device.remove(this.id as number);
|
|
this.$emit("removed", this.name);
|
|
(this.$refs["modal"] as any).close();
|
|
} catch (e) {
|
|
handleError(e, "remove failed");
|
|
}
|
|
},
|
|
open() {
|
|
this.isModalVisible = true;
|
|
},
|
|
close() {
|
|
this.$emit("close");
|
|
this.isModalVisible = false;
|
|
},
|
|
templateChanged(e: Event) {
|
|
const value = (e.target as HTMLSelectElement).value as ConfigType;
|
|
this.reset();
|
|
if (this.isYamlInput(value)) {
|
|
this.values.type = value;
|
|
this.values.yaml = this.defaultYaml(value);
|
|
}
|
|
},
|
|
defaultYaml(type: ConfigType) {
|
|
switch (type) {
|
|
case ConfigType.Custom:
|
|
return this.isHeating ? customHeaterYaml : customChargerYaml;
|
|
case ConfigType.Heatpump:
|
|
return heatpumpYaml;
|
|
case ConfigType.SwitchSocket:
|
|
return this.isHeating ? switchsocketHeaterYaml : switchsocketChargerYaml;
|
|
case ConfigType.SgReady:
|
|
return sgreadyYaml;
|
|
case ConfigType.SgReadyBoost:
|
|
return sgreadyBoostYaml;
|
|
default: // template
|
|
return "";
|
|
}
|
|
},
|
|
isYamlInput(type: ConfigType) {
|
|
return [
|
|
ConfigType.Custom,
|
|
ConfigType.Heatpump,
|
|
ConfigType.SwitchSocket,
|
|
ConfigType.SgReady,
|
|
ConfigType.SgReadyBoost,
|
|
].includes(type);
|
|
},
|
|
},
|
|
});
|
|
</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;
|
|
}
|
|
.addButton {
|
|
min-height: auto;
|
|
}
|
|
</style>
|