Config UI: add ext meter (#24044)
This commit is contained in:
parent
57f74ecc26
commit
4645b67e0c
12 changed files with 504 additions and 170 deletions
|
|
@ -106,7 +106,7 @@ export default defineComponent({
|
|||
this.currentTemplate?.Params.some((p: TemplateParam) => p.Name === "connector") &&
|
||||
this.currentTemplate?.Params.some((p: TemplateParam) => p.Name === "stationid");
|
||||
if (isOcpp && this.currentValues) {
|
||||
return `ws://${window.location.hostname}:8887/${this.currentValues["stationid"] || ""}`;
|
||||
return `ws://${window.location.hostname}:8887/${this.currentValues.stationid || ""}`;
|
||||
}
|
||||
return null;
|
||||
},
|
||||
|
|
@ -174,9 +174,9 @@ export default defineComponent({
|
|||
);
|
||||
},
|
||||
transformApiData(data: ApiData): ApiData {
|
||||
if (this.isYamlInput(data["type"] as ConfigType)) {
|
||||
if (data.type && this.isYamlInput(data.type)) {
|
||||
// Icon is extracted from yaml on GET for UI purpose only. Don't write it back.
|
||||
delete data["icon"];
|
||||
delete data.icon;
|
||||
}
|
||||
return data;
|
||||
},
|
||||
|
|
@ -189,7 +189,7 @@ export default defineComponent({
|
|||
ConfigType.SgReadyBoost,
|
||||
].includes(type);
|
||||
},
|
||||
async handleTemplateChange(e: Event, values: DeviceValues) {
|
||||
handleTemplateChange(e: Event, values: DeviceValues) {
|
||||
const value = (e.target as HTMLSelectElement).value as ConfigType;
|
||||
if (this.isYamlInput(value)) {
|
||||
values.type = value;
|
||||
|
|
@ -210,8 +210,8 @@ export default defineComponent({
|
|||
}
|
||||
});
|
||||
// default heater icon
|
||||
if (hasParam("icon") && values["icon"] === undefined) {
|
||||
values["icon"] = "heater";
|
||||
if (hasParam("icon") && values.icon === undefined) {
|
||||
values.icon = "heater";
|
||||
}
|
||||
}
|
||||
},
|
||||
|
|
|
|||
|
|
@ -175,11 +175,15 @@ export default defineComponent({
|
|||
// Optional: provide template options from parent (to avoid circular dependency)
|
||||
provideTemplateOptions: Function as PropType<(products: Product[]) => TemplateGroup[]>,
|
||||
// Optional: handle template change (receives event and values, allows setting values.yaml)
|
||||
onTemplateChange: Function as PropType<(e: Event, values: DeviceValues) => Promise<void>>,
|
||||
onTemplateChange: Function as PropType<(e: Event, values: DeviceValues) => void>,
|
||||
// Optional: default template to select when opening modal for new devices
|
||||
defaultTemplate: String,
|
||||
// Optional: callback after configuration is loaded (receives values)
|
||||
onConfigurationLoaded: Function as PropType<(values: DeviceValues) => void>,
|
||||
// Optional: external template selection control (for parent to reset template)
|
||||
externalTemplate: String as PropType<string | null>,
|
||||
},
|
||||
emits: ["added", "updated", "removed", "close", "template-changed"],
|
||||
emits: ["added", "updated", "removed", "close", "template-changed", "update:externalTemplate"],
|
||||
data() {
|
||||
return {
|
||||
isModalVisible: false,
|
||||
|
|
@ -289,10 +293,7 @@ export default defineComponent({
|
|||
return this.templateName || this.showYamlInput;
|
||||
},
|
||||
showYamlInput() {
|
||||
if (this.isYamlInputType) {
|
||||
return this.isYamlInputType(this.values.type);
|
||||
}
|
||||
return this.values.type === ConfigType.Custom;
|
||||
return this.isYamlInputTypeByValue(this.values.type);
|
||||
},
|
||||
showTemplateSelector() {
|
||||
return this.computedTemplateOptions.length > 0;
|
||||
|
|
@ -309,15 +310,22 @@ export default defineComponent({
|
|||
this.loadProducts();
|
||||
if (this.id !== undefined) {
|
||||
this.loadConfiguration();
|
||||
} else {
|
||||
// For new devices, apply defaults immediately (e.g., default icons based on meter type)
|
||||
this.applyDefaults();
|
||||
}
|
||||
}
|
||||
},
|
||||
templateName(newValue, oldValue) {
|
||||
// Sync back to parent if using externalTemplate
|
||||
if (this.externalTemplate !== undefined && newValue !== this.externalTemplate) {
|
||||
this.$emit("update:externalTemplate", newValue);
|
||||
}
|
||||
|
||||
console.log("templateName changed", { newValue, oldValue });
|
||||
// Reset values when template changes (except on initial load or when switching to YAML input)
|
||||
// YAML input types set values.type and values.yaml in handleTemplateChange callback
|
||||
const isYamlInput =
|
||||
this.isYamlInputType && newValue && this.isYamlInputType(newValue as any);
|
||||
if (oldValue != null && !isYamlInput) {
|
||||
if (oldValue != null) {
|
||||
if (this.preserveOnTemplateChange) {
|
||||
const preserved: Record<string, any> = {};
|
||||
this.preserveOnTemplateChange.forEach((field) => {
|
||||
|
|
@ -331,7 +339,11 @@ export default defineComponent({
|
|||
this.reset();
|
||||
}
|
||||
}
|
||||
this.loadTemplate();
|
||||
|
||||
const isYamlInput = this.isYamlInputTypeByValue(newValue as ConfigType);
|
||||
if (!isYamlInput) {
|
||||
this.loadTemplate();
|
||||
}
|
||||
},
|
||||
usage() {
|
||||
// Reload products when usage changes (e.g., meter type selection)
|
||||
|
|
@ -339,6 +351,19 @@ export default defineComponent({
|
|||
// Apply defaults when usage changes (e.g., set default icon for meter type)
|
||||
this.applyDefaults();
|
||||
},
|
||||
externalTemplate(newValue) {
|
||||
// Allow parent to control template selection
|
||||
if (newValue !== this.templateName) {
|
||||
this.templateName = newValue;
|
||||
}
|
||||
},
|
||||
showMainContent(visible) {
|
||||
// When main content becomes visible (e.g., meter type selected in MeterModal),
|
||||
// apply defaults like icon based on type
|
||||
if (visible) {
|
||||
this.applyDefaults();
|
||||
}
|
||||
},
|
||||
values: {
|
||||
handler() {
|
||||
this.test = initialTestState();
|
||||
|
|
@ -360,13 +385,18 @@ export default defineComponent({
|
|||
this.values.type = device.type;
|
||||
this.values.deviceProduct = device.deviceProduct;
|
||||
if (device.deviceTitle !== undefined) {
|
||||
this.values["deviceTitle"] = device.deviceTitle;
|
||||
this.values.deviceTitle = device.deviceTitle;
|
||||
}
|
||||
if (device.deviceIcon !== undefined) {
|
||||
this.values["deviceIcon"] = device.deviceIcon;
|
||||
this.values.deviceIcon = device.deviceIcon;
|
||||
}
|
||||
this.applyDefaults();
|
||||
this.templateName = this.values.template;
|
||||
|
||||
// Allow parent to handle post-load logic
|
||||
if (this.onConfigurationLoaded) {
|
||||
this.onConfigurationLoaded(this.values);
|
||||
}
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
}
|
||||
|
|
@ -485,13 +515,13 @@ export default defineComponent({
|
|||
this.$emit("close");
|
||||
this.isModalVisible = false;
|
||||
},
|
||||
async handleTemplateChange(e: Event) {
|
||||
// Allow parent to handle custom logic (e.g., loading default YAML)
|
||||
if (this.onTemplateChange) {
|
||||
await this.onTemplateChange(e, this.values);
|
||||
}
|
||||
// Emit to parent for notification
|
||||
this.$emit("template-changed", e);
|
||||
handleTemplateChange(e: Event) {
|
||||
// ensure this triggers after tempateName watcher
|
||||
this.$nextTick(() => {
|
||||
if (this.onTemplateChange) {
|
||||
this.onTemplateChange(e, this.values);
|
||||
}
|
||||
});
|
||||
},
|
||||
handleSave(force: boolean) {
|
||||
if (this.isNew) {
|
||||
|
|
@ -503,6 +533,12 @@ export default defineComponent({
|
|||
handleRemove() {
|
||||
this.remove();
|
||||
},
|
||||
isYamlInputTypeByValue(value: ConfigType): boolean {
|
||||
if (this.isYamlInputType) {
|
||||
return this.isYamlInputType(value);
|
||||
}
|
||||
return value === ConfigType.Custom;
|
||||
},
|
||||
},
|
||||
});
|
||||
</script>
|
||||
|
|
|
|||
|
|
@ -6,6 +6,7 @@
|
|||
ref="select"
|
||||
v-model="modelProxy"
|
||||
class="form-select w-100"
|
||||
:disabled="disabled"
|
||||
@change="changed"
|
||||
>
|
||||
<template v-for="group in groups" :key="group.label">
|
||||
|
|
@ -63,6 +64,7 @@ export default defineComponent({
|
|||
modelValue: String as PropType<string | null>,
|
||||
productName: String,
|
||||
groups: Array as PropType<TemplateGroup[]>,
|
||||
disabled: Boolean,
|
||||
},
|
||||
emits: ["update:modelValue", "change"],
|
||||
computed: {
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
import type { DeviceType } from "@/types/evcc";
|
||||
import type { DeviceType, MeterTemplateUsage } from "@/types/evcc";
|
||||
import { ConfigType } from "@/types/evcc";
|
||||
import api from "@/api";
|
||||
|
||||
|
|
@ -15,7 +15,7 @@ export type Template = {
|
|||
};
|
||||
};
|
||||
|
||||
export type TemplateType = "vehicle" | "battery" | "grid" | "pv" | "charger" | "aux" | "ext";
|
||||
export type TemplateParamUsage = "vehicle" | "battery" | "grid" | "pv" | "charger" | "aux" | "ext";
|
||||
|
||||
export type TemplateParam = {
|
||||
Name: string;
|
||||
|
|
@ -24,7 +24,7 @@ export type TemplateParam = {
|
|||
Deprecated: boolean;
|
||||
Default?: string | number | boolean;
|
||||
Choice?: string[];
|
||||
Usages?: TemplateType[];
|
||||
Usages?: TemplateParamUsage[];
|
||||
};
|
||||
|
||||
export type ModbusCapability = "rs485" | "tcpip";
|
||||
|
|
@ -38,14 +38,27 @@ export type ModbusParam = TemplateParam & {
|
|||
|
||||
export type DeviceValues = {
|
||||
type: ConfigType;
|
||||
icon: string | undefined;
|
||||
deviceProduct: string | undefined;
|
||||
yaml: string | undefined;
|
||||
icon?: string;
|
||||
deviceProduct?: string;
|
||||
yaml?: string;
|
||||
template: string | null;
|
||||
deviceTitle?: string;
|
||||
deviceIcon?: string;
|
||||
usage?: MeterTemplateUsage;
|
||||
heating?: boolean;
|
||||
integrateddevice?: boolean;
|
||||
stationid?: string;
|
||||
[key: string]: any;
|
||||
};
|
||||
|
||||
export type ApiData = Record<string, any>;
|
||||
export type ApiData = {
|
||||
type?: ConfigType;
|
||||
icon?: string;
|
||||
usage?: MeterTemplateUsage;
|
||||
title?: string;
|
||||
identifiers?: string[];
|
||||
[key: string]: any;
|
||||
};
|
||||
|
||||
export function handleError(e: any, msg: string) {
|
||||
console.error(e);
|
||||
|
|
|
|||
94
assets/js/components/Config/MeterCard.vue
Normal file
94
assets/js/components/Config/MeterCard.vue
Normal file
|
|
@ -0,0 +1,94 @@
|
|||
<template>
|
||||
<DeviceCard
|
||||
:title="cardTitle"
|
||||
:name="meter.name"
|
||||
:editable="!!meter.id"
|
||||
:error="hasError"
|
||||
:data-testid="meterType"
|
||||
@edit="$emit('edit', meterType, meter.id)"
|
||||
>
|
||||
<template #icon>
|
||||
<VehicleIcon v-if="isVehicleIcon" :name="iconName" />
|
||||
<component :is="iconComponent" v-else />
|
||||
</template>
|
||||
<template #tags>
|
||||
<DeviceTags :tags="tags" />
|
||||
</template>
|
||||
</DeviceCard>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import DeviceCard from "./DeviceCard.vue";
|
||||
import DeviceTags from "./DeviceTags.vue";
|
||||
import VehicleIcon from "../VehicleIcon";
|
||||
|
||||
export default {
|
||||
name: "MeterCard",
|
||||
components: {
|
||||
DeviceCard,
|
||||
DeviceTags,
|
||||
VehicleIcon,
|
||||
},
|
||||
props: {
|
||||
meter: {
|
||||
type: Object,
|
||||
required: true,
|
||||
},
|
||||
meterType: {
|
||||
type: String,
|
||||
required: true,
|
||||
validator: (value) => ["grid", "pv", "battery", "aux", "ext"].includes(value),
|
||||
},
|
||||
hasError: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
tags: {
|
||||
type: Object,
|
||||
default: () => ({}),
|
||||
},
|
||||
},
|
||||
emits: ["edit"],
|
||||
computed: {
|
||||
cardTitle() {
|
||||
if (this.meter.deviceTitle) {
|
||||
return this.meter.deviceTitle;
|
||||
}
|
||||
if (this.meter.config?.template) {
|
||||
return this.meter.config.template;
|
||||
}
|
||||
return this.fallbackTitle;
|
||||
},
|
||||
fallbackTitle() {
|
||||
const titleMap = {
|
||||
grid: this.$t("config.grid.title"),
|
||||
pv: this.$t("config.devices.solarSystem"),
|
||||
battery: this.$t("config.devices.batteryStorage"),
|
||||
aux: this.$t("config.devices.auxMeter"),
|
||||
ext: this.$t("config.devices.extMeter"),
|
||||
};
|
||||
return titleMap[this.meterType];
|
||||
},
|
||||
isVehicleIcon() {
|
||||
return this.meterType === "aux" || this.meterType === "ext";
|
||||
},
|
||||
iconComponent() {
|
||||
const iconMap = {
|
||||
grid: "shopicon-regular-powersupply",
|
||||
pv: "shopicon-regular-sun",
|
||||
battery: "shopicon-regular-batterythreequarters",
|
||||
};
|
||||
return iconMap[this.meterType];
|
||||
},
|
||||
iconName() {
|
||||
if (this.meterType === "aux") {
|
||||
return this.meter.deviceIcon || "smartconsumer";
|
||||
}
|
||||
if (this.meterType === "ext") {
|
||||
return this.meter.deviceIcon || "generic";
|
||||
}
|
||||
return null;
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
|
@ -1,7 +1,7 @@
|
|||
<template>
|
||||
<DeviceModalBase
|
||||
:id="id"
|
||||
ref="deviceModalBase"
|
||||
v-model:external-template="selectedTemplate"
|
||||
modal-id="meterModal"
|
||||
device-type="meter"
|
||||
:fade="fade"
|
||||
|
|
@ -9,7 +9,6 @@
|
|||
:modal-title="modalTitle"
|
||||
:provide-template-options="provideTemplateOptions"
|
||||
:initial-values="initialValues"
|
||||
:is-yaml-input-type="isYamlInput"
|
||||
:transform-api-data="transformApiData"
|
||||
:filter-template-params="filterTemplateParams"
|
||||
:on-template-change="handleTemplateChange"
|
||||
|
|
@ -17,7 +16,8 @@
|
|||
:apply-custom-defaults="applyCustomDefaults"
|
||||
:custom-fields="customFields"
|
||||
:preserve-on-template-change="preserveFields"
|
||||
:usage="meterType || undefined"
|
||||
:usage="templateUsage"
|
||||
:on-configuration-loaded="onConfigurationLoaded"
|
||||
@added="handleAdded"
|
||||
@updated="$emit('updated')"
|
||||
@removed="handleRemoved"
|
||||
|
|
@ -39,17 +39,13 @@
|
|||
<p v-if="hasDescription" class="mt-0 mb-4">
|
||||
{{ $t(`config.${meterType}.description`) }}
|
||||
</p>
|
||||
<div v-if="meterType === 'ext'" class="alert alert-warning mb-4" role="alert">
|
||||
<strong>Work in Progress:</strong> This feature is not yet available.
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<template #before-template="{ values }">
|
||||
<FormRow
|
||||
v-if="hasDeviceTitle"
|
||||
id="meterParamDeviceTitle"
|
||||
label="Title"
|
||||
help="Will be displayed in the user interface"
|
||||
:label="$t('config.meter.titleLabel')"
|
||||
>
|
||||
<PropertyField
|
||||
id="meterParamDeviceTitle"
|
||||
|
|
@ -60,7 +56,11 @@
|
|||
required
|
||||
/>
|
||||
</FormRow>
|
||||
<FormRow v-if="hasDeviceIcon" id="meterParamDeviceIcon" label="Icon">
|
||||
<FormRow
|
||||
v-if="hasDeviceIcon"
|
||||
id="meterParamDeviceIcon"
|
||||
:label="$t('config.icon.label')"
|
||||
>
|
||||
<PropertyField
|
||||
id="meterParamDeviceIcon"
|
||||
v-model="values.deviceIcon"
|
||||
|
|
@ -71,6 +71,20 @@
|
|||
required
|
||||
/>
|
||||
</FormRow>
|
||||
<FormRow
|
||||
v-if="meterType === 'ext'"
|
||||
id="meterParamExtMeterUsage"
|
||||
:label="$t('config.meter.usage.label')"
|
||||
>
|
||||
<PropertyField
|
||||
id="meterParamExtMeterUsage"
|
||||
v-model="extMeterUsage"
|
||||
:choice="extMeterUsageOptions"
|
||||
:required="!!extMeterUsage"
|
||||
:disabled="!isNew"
|
||||
@change="extMeterUsageChanged"
|
||||
/>
|
||||
</FormRow>
|
||||
</template>
|
||||
</DeviceModalBase>
|
||||
</template>
|
||||
|
|
@ -82,7 +96,7 @@ import PropertyField from "./PropertyField.vue";
|
|||
import NewDeviceButton from "./NewDeviceButton.vue";
|
||||
import DeviceModalBase from "./DeviceModal/DeviceModalBase.vue";
|
||||
import { ICONS } from "../VehicleIcon/VehicleIcon.vue";
|
||||
import { ConfigType, type SelectedMeterType } from "@/types/evcc";
|
||||
import { ConfigType, type MeterType, type MeterTemplateUsage } from "@/types/evcc";
|
||||
import type { ModalFade } from "../Helper/GenericModal.vue";
|
||||
import {
|
||||
type DeviceValues,
|
||||
|
|
@ -90,9 +104,9 @@ import {
|
|||
type Product,
|
||||
type TemplateParam,
|
||||
type ApiData,
|
||||
type TemplateType,
|
||||
} from "./DeviceModal";
|
||||
import { customTemplateOption, type TemplateGroup } from "./DeviceModal/TemplateSelector.vue";
|
||||
import defaultMeterYaml from "./defaultYaml/meter.yaml?raw";
|
||||
|
||||
const initialValues = {
|
||||
type: ConfigType.Template,
|
||||
|
|
@ -108,7 +122,7 @@ const CUSTOM_FIELDS = ["usage", "modbus"];
|
|||
|
||||
const defaultIcons: Record<string, string> = {
|
||||
aux: "smartconsumer",
|
||||
ext: "meter",
|
||||
ext: "generic",
|
||||
};
|
||||
|
||||
export default defineComponent({
|
||||
|
|
@ -121,21 +135,17 @@ export default defineComponent({
|
|||
},
|
||||
props: {
|
||||
id: Number,
|
||||
type: {
|
||||
type: String as () => SelectedMeterType | undefined,
|
||||
default: undefined,
|
||||
},
|
||||
typeChoices: {
|
||||
type: Array as () => string[],
|
||||
default: () => ["pv", "battery", "aux", "ext"],
|
||||
},
|
||||
type: { type: String as PropType<MeterType>, default: null },
|
||||
typeChoices: { type: Array as () => MeterType[], default: () => [] },
|
||||
fade: String as PropType<ModalFade>,
|
||||
isSponsor: Boolean,
|
||||
},
|
||||
emits: ["added", "updated", "removed", "close"],
|
||||
data() {
|
||||
return {
|
||||
selectedType: null as string | null,
|
||||
selectedType: null as MeterType | null,
|
||||
extMeterUsage: "charge" as MeterTemplateUsage,
|
||||
selectedTemplate: null as string | null,
|
||||
iconChoices: ICONS,
|
||||
initialValues,
|
||||
customFields: CUSTOM_FIELDS,
|
||||
|
|
@ -153,10 +163,21 @@ export default defineComponent({
|
|||
}
|
||||
return this.$t(`config.${this.meterType}.titleEdit`);
|
||||
},
|
||||
meterType(): Exclude<TemplateType, "vehicle" | "charger"> | null {
|
||||
// @ts-expect-error either this.type or this.selectedType is given
|
||||
meterType(): MeterType | null {
|
||||
return this.type || this.selectedType;
|
||||
},
|
||||
templateUsage(): MeterTemplateUsage | undefined {
|
||||
if (!this.meterType) return undefined;
|
||||
|
||||
// For ext meters, the user selects the template usage explicitly
|
||||
// For other meter types, the meter type IS the template usage
|
||||
if (this.meterType === "ext") {
|
||||
return this.extMeterUsage;
|
||||
}
|
||||
// For non-ext meters, meterType directly maps to template usage
|
||||
// (grid->grid, pv->pv, battery->battery, charge->charge, aux->aux)
|
||||
return this.meterType;
|
||||
},
|
||||
hasDeviceTitle(): boolean {
|
||||
return ["pv", "battery", "aux", "ext"].includes(this.meterType || "");
|
||||
},
|
||||
|
|
@ -169,9 +190,21 @@ export default defineComponent({
|
|||
isNew(): boolean {
|
||||
return this.id === undefined;
|
||||
},
|
||||
extMeterUsageOptions() {
|
||||
return ["grid", "pv", "battery", "charge", "aux"].map((key) => ({
|
||||
name: this.$t(`config.meter.usage.${key}`),
|
||||
key,
|
||||
}));
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
selectType(type: string) {
|
||||
onConfigurationLoaded(values: DeviceValues) {
|
||||
// Restore extMeterUsage when editing an existing ext meter
|
||||
if (this.meterType === "ext" && values.usage) {
|
||||
this.extMeterUsage = values.usage;
|
||||
}
|
||||
},
|
||||
selectType(type: MeterType) {
|
||||
this.selectedType = type;
|
||||
},
|
||||
provideTemplateOptions(products: Product[]): TemplateGroup[] {
|
||||
|
|
@ -193,7 +226,9 @@ export default defineComponent({
|
|||
const filtered = params.filter(
|
||||
(p) =>
|
||||
!CUSTOM_FIELDS.includes(p.Name) &&
|
||||
(p.Usages && this.meterType ? p.Usages.includes(this.meterType) : true)
|
||||
(p.Usages && this.templateUsage
|
||||
? p.Usages.includes(this.templateUsage as any)
|
||||
: true)
|
||||
);
|
||||
|
||||
// Make capacity non-advanced for battery meters
|
||||
|
|
@ -206,27 +241,33 @@ export default defineComponent({
|
|||
},
|
||||
transformApiData(data: ApiData, values: DeviceValues): ApiData {
|
||||
if (values.type === ConfigType.Template) {
|
||||
data["usage"] = this.meterType || undefined;
|
||||
// Set the template usage (what the template should do)
|
||||
// For ext meters: user-selected usage (grid, pv, battery, charge, aux)
|
||||
// For other meters: meterType itself is the usage
|
||||
data.usage = this.templateUsage;
|
||||
}
|
||||
return data;
|
||||
},
|
||||
isYamlInput(type: ConfigType): boolean {
|
||||
return type === ConfigType.Custom;
|
||||
},
|
||||
async handleTemplateChange(e: Event, values: DeviceValues) {
|
||||
handleTemplateChange(e: Event, values: DeviceValues) {
|
||||
console.log("[MeterModal] handleTemplateChange", { e, values });
|
||||
const value = (e.target as HTMLSelectElement).value;
|
||||
if (value === ConfigType.Custom) {
|
||||
const defaultYaml = await import("./defaultYaml/meter.yaml?raw");
|
||||
values.type = ConfigType.Custom;
|
||||
values.yaml = defaultYaml.default;
|
||||
values.yaml = defaultMeterYaml;
|
||||
console.log("[MeterModal] set type/yaml", { type: values.type, yaml: values.yaml });
|
||||
}
|
||||
},
|
||||
applyCustomDefaults(_template: Template | null, values: DeviceValues) {
|
||||
// Apply default icon when template is loaded
|
||||
if (this.meterType && !values["deviceIcon"]) {
|
||||
values["deviceIcon"] = defaultIcons[this.meterType] || "";
|
||||
// Apply default icon when template is loaded or meter type is selected
|
||||
if (this.meterType && !values.deviceIcon) {
|
||||
values.deviceIcon = defaultIcons[this.meterType] || "";
|
||||
}
|
||||
},
|
||||
extMeterUsageChanged() {
|
||||
// When ext meter usage changes, reset template selection to force user to reselect
|
||||
// This triggers product reload via effectiveUsage computed property change
|
||||
this.selectedTemplate = null;
|
||||
},
|
||||
handleAdded(name: string) {
|
||||
this.$emit("added", this.meterType, name);
|
||||
},
|
||||
|
|
@ -235,6 +276,7 @@ export default defineComponent({
|
|||
},
|
||||
handleClose() {
|
||||
this.selectedType = null;
|
||||
this.extMeterUsage = "charge";
|
||||
this.$emit("close");
|
||||
},
|
||||
},
|
||||
|
|
|
|||
|
|
@ -7,7 +7,6 @@
|
|||
:modal-title="$t(`config.vehicle.${isNew ? 'titleAdd' : 'titleEdit'}`)"
|
||||
:provide-template-options="provideTemplateOptions"
|
||||
:initial-values="initialValues"
|
||||
:is-yaml-input-type="isYamlInput"
|
||||
:transform-api-data="transformApiData"
|
||||
:filter-template-params="filterTemplateParams"
|
||||
:on-template-change="handleTemplateChange"
|
||||
|
|
@ -141,6 +140,7 @@ import DeviceModalBase from "./DeviceModal/DeviceModalBase.vue";
|
|||
import { ConfigType } from "@/types/evcc";
|
||||
import { customTemplateOption, type TemplateGroup } from "./DeviceModal/TemplateSelector.vue";
|
||||
import type { Product, ApiData, DeviceValues, TemplateParam } from "./DeviceModal";
|
||||
import defaultVehicleYaml from "./defaultYaml/vehicle.yaml?raw";
|
||||
|
||||
const initialValues = {
|
||||
type: ConfigType.Template,
|
||||
|
|
@ -229,23 +229,19 @@ export default defineComponent({
|
|||
},
|
||||
transformApiData(data: ApiData, values: DeviceValues): ApiData {
|
||||
if (values.type === ConfigType.Custom) {
|
||||
delete data["icon"];
|
||||
delete data["title"];
|
||||
delete data.icon;
|
||||
delete data.title;
|
||||
}
|
||||
if (Array.isArray(data["identifiers"])) {
|
||||
data["identifiers"] = data["identifiers"].map((i) => i.trim()).filter((i) => i);
|
||||
if (Array.isArray(data.identifiers)) {
|
||||
data.identifiers = data.identifiers.map((i) => i.trim()).filter((i) => i);
|
||||
}
|
||||
return data;
|
||||
},
|
||||
isYamlInput(type: ConfigType) {
|
||||
return type === ConfigType.Custom;
|
||||
},
|
||||
async handleTemplateChange(e: Event, values: DeviceValues) {
|
||||
handleTemplateChange(e: Event, values: DeviceValues) {
|
||||
const value = (e.target as HTMLSelectElement).value;
|
||||
if (value === ConfigType.Custom) {
|
||||
const defaultYaml = await import("./defaultYaml/vehicle.yaml?raw");
|
||||
values.type = ConfigType.Custom;
|
||||
values.yaml = defaultYaml.default;
|
||||
values.yaml = defaultVehicleYaml;
|
||||
}
|
||||
},
|
||||
},
|
||||
|
|
|
|||
|
|
@ -415,7 +415,8 @@ export interface SelectOption<T> {
|
|||
}
|
||||
|
||||
export type DeviceType = "charger" | "meter" | "vehicle" | "loadpoint";
|
||||
export type SelectedMeterType = "grid" | "pv" | "battery" | "charge" | "aux" | "ext";
|
||||
export type MeterType = "grid" | "pv" | "battery" | "charge" | "aux" | "ext";
|
||||
export type MeterTemplateUsage = "grid" | "pv" | "battery" | "charge" | "aux";
|
||||
|
||||
// see https://stackoverflow.com/a/54178819
|
||||
type Omit<T, K extends keyof T> = Pick<T, Exclude<keyof T, K>>;
|
||||
|
|
|
|||
|
|
@ -80,22 +80,14 @@
|
|||
|
||||
<h2 class="my-4 mt-5">{{ $t("config.section.grid") }} 🧪</h2>
|
||||
<div class="p-0 config-list">
|
||||
<DeviceCard
|
||||
<MeterCard
|
||||
v-if="gridMeter"
|
||||
:title="$t('config.grid.title')"
|
||||
:name="gridMeter.name"
|
||||
:editable="!!gridMeter.id"
|
||||
:error="hasDeviceError('meter', gridMeter.name)"
|
||||
data-testid="grid"
|
||||
@edit="editMeter('grid', gridMeter.id)"
|
||||
>
|
||||
<template #icon>
|
||||
<shopicon-regular-powersupply></shopicon-regular-powersupply>
|
||||
</template>
|
||||
<template #tags>
|
||||
<DeviceTags :tags="deviceTags('meter', gridMeter.name)" />
|
||||
</template>
|
||||
</DeviceCard>
|
||||
:meter="gridMeter"
|
||||
meter-type="grid"
|
||||
:has-error="hasDeviceError('meter', gridMeter.name)"
|
||||
:tags="deviceTags('meter', gridMeter.name)"
|
||||
@edit="editMeter"
|
||||
/>
|
||||
<NewDeviceButton
|
||||
v-else
|
||||
:title="$t('config.main.addGrid')"
|
||||
|
|
@ -126,48 +118,24 @@
|
|||
</div>
|
||||
<h2 class="my-4 mt-5">{{ $t("config.section.meter") }} 🧪</h2>
|
||||
<div class="p-0 config-list">
|
||||
<DeviceCard
|
||||
<MeterCard
|
||||
v-for="meter in pvMeters"
|
||||
:key="meter.name"
|
||||
:title="
|
||||
meter.deviceTitle ||
|
||||
meter.config?.template ||
|
||||
$t('config.devices.solarSystem')
|
||||
"
|
||||
:name="meter.name"
|
||||
:editable="!!meter.id"
|
||||
:error="hasDeviceError('meter', meter.name)"
|
||||
data-testid="pv"
|
||||
@edit="editMeter('pv', meter.id)"
|
||||
>
|
||||
<template #icon>
|
||||
<shopicon-regular-sun></shopicon-regular-sun>
|
||||
</template>
|
||||
<template #tags>
|
||||
<DeviceTags :tags="deviceTags('meter', meter.name)" />
|
||||
</template>
|
||||
</DeviceCard>
|
||||
<DeviceCard
|
||||
:meter="meter"
|
||||
meter-type="pv"
|
||||
:has-error="hasDeviceError('meter', meter.name)"
|
||||
:tags="deviceTags('meter', meter.name)"
|
||||
@edit="editMeter"
|
||||
/>
|
||||
<MeterCard
|
||||
v-for="meter in batteryMeters"
|
||||
:key="meter.name"
|
||||
:title="
|
||||
meter.deviceTitle ||
|
||||
meter.config?.template ||
|
||||
$t('config.devices.batteryStorage')
|
||||
"
|
||||
:name="meter.name"
|
||||
:editable="!!meter.id"
|
||||
:error="hasDeviceError('meter', meter.name)"
|
||||
data-testid="battery"
|
||||
@edit="editMeter('battery', meter.id)"
|
||||
>
|
||||
<template #icon>
|
||||
<shopicon-regular-batterythreequarters></shopicon-regular-batterythreequarters>
|
||||
</template>
|
||||
<template #tags>
|
||||
<DeviceTags :tags="deviceTags('meter', meter.name)" />
|
||||
</template>
|
||||
</DeviceCard>
|
||||
:meter="meter"
|
||||
meter-type="battery"
|
||||
:has-error="hasDeviceError('meter', meter.name)"
|
||||
:tags="deviceTags('meter', meter.name)"
|
||||
@edit="editMeter"
|
||||
/>
|
||||
<NewDeviceButton
|
||||
:title="$t('config.main.addPvBattery')"
|
||||
@click="addSolarBatteryMeter"
|
||||
|
|
@ -176,27 +144,24 @@
|
|||
|
||||
<h2 class="my-4 mt-5">{{ $t("config.section.additionalMeter") }} 🧪</h2>
|
||||
<div class="p-0 config-list">
|
||||
<DeviceCard
|
||||
<MeterCard
|
||||
v-for="meter in auxMeters"
|
||||
:key="meter.name"
|
||||
:title="
|
||||
meter.deviceTitle ||
|
||||
meter.config?.template ||
|
||||
$t('config.devices.auxMeter')
|
||||
"
|
||||
:name="meter.name"
|
||||
:editable="!!meter.id"
|
||||
:error="hasDeviceError('meter', meter.name)"
|
||||
data-testid="aux"
|
||||
@edit="editMeter('aux', meter.id)"
|
||||
>
|
||||
<template #icon>
|
||||
<VehicleIcon :name="meter.deviceIcon || 'smartconsumer'" />
|
||||
</template>
|
||||
<template #tags>
|
||||
<DeviceTags :tags="deviceTags('meter', meter.name)" />
|
||||
</template>
|
||||
</DeviceCard>
|
||||
:meter="meter"
|
||||
meter-type="aux"
|
||||
:has-error="hasDeviceError('meter', meter.name)"
|
||||
:tags="deviceTags('meter', meter.name)"
|
||||
@edit="editMeter"
|
||||
/>
|
||||
<MeterCard
|
||||
v-for="meter in extMeters"
|
||||
:key="meter.name"
|
||||
:meter="meter"
|
||||
meter-type="ext"
|
||||
:has-error="hasDeviceError('meter', meter.name)"
|
||||
:tags="deviceTags('meter', meter.name)"
|
||||
@edit="editMeter"
|
||||
/>
|
||||
<NewDeviceButton
|
||||
:title="$t('config.main.addAdditional')"
|
||||
@click="newAdditionalMeter"
|
||||
|
|
@ -436,6 +401,7 @@ import LoadpointModal from "../components/Config/LoadpointModal.vue";
|
|||
import LoadpointIcon from "../components/MaterialIcon/Loadpoint.vue";
|
||||
import MessagingModal from "../components/Config/MessagingModal.vue";
|
||||
import MeterModal from "../components/Config/MeterModal.vue";
|
||||
import MeterCard from "../components/Config/MeterCard.vue";
|
||||
import Modal from "bootstrap/js/dist/modal";
|
||||
import ModbusProxyIcon from "../components/MaterialIcon/ModbusProxy.vue";
|
||||
import ModbusProxyModal from "../components/Config/ModbusProxyModal.vue";
|
||||
|
|
@ -461,7 +427,7 @@ import type {
|
|||
ConfigMeter,
|
||||
LoadpointType,
|
||||
Timeout,
|
||||
SelectedMeterType,
|
||||
MeterType,
|
||||
SiteConfig,
|
||||
DeviceType,
|
||||
} from "@/types/evcc";
|
||||
|
|
@ -495,6 +461,7 @@ export default defineComponent({
|
|||
InfluxModal,
|
||||
MessagingModal,
|
||||
MeterModal,
|
||||
MeterCard,
|
||||
LoadpointModal,
|
||||
LoadpointIcon,
|
||||
ModbusProxyIcon,
|
||||
|
|
@ -526,8 +493,8 @@ export default defineComponent({
|
|||
circuits: [] as ConfigCircuit[],
|
||||
selectedVehicleId: undefined as number | undefined,
|
||||
selectedMeterId: undefined as number | undefined,
|
||||
selectedMeterType: undefined as SelectedMeterType | undefined,
|
||||
selectedMeterTypeChoices: [] as string[],
|
||||
selectedMeterType: undefined as MeterType | undefined,
|
||||
selectedMeterTypeChoices: [] as MeterType[],
|
||||
selectedChargerId: undefined as number | undefined,
|
||||
selectedLoadpointId: undefined as number | undefined,
|
||||
selectedLoadpointType: undefined as LoadpointType | undefined,
|
||||
|
|
@ -838,12 +805,12 @@ export default defineComponent({
|
|||
this.loadpointModal().hide();
|
||||
this.$nextTick(() => this.editMeter("charge", meter?.id));
|
||||
},
|
||||
editMeter(type: SelectedMeterType, id?: number) {
|
||||
editMeter(type: MeterType, id?: number) {
|
||||
this.selectedMeterType = type;
|
||||
this.selectedMeterId = id;
|
||||
this.$nextTick(() => this.meterModal().show());
|
||||
},
|
||||
newMeter(type: SelectedMeterType) {
|
||||
newMeter(type: MeterType) {
|
||||
this.selectedMeterId = undefined;
|
||||
this.selectedMeterType = type;
|
||||
this.$nextTick(() => this.meterModal().show());
|
||||
|
|
@ -851,13 +818,13 @@ export default defineComponent({
|
|||
addSolarBatteryMeter() {
|
||||
this.selectedMeterId = undefined;
|
||||
this.selectedMeterType = undefined;
|
||||
this.selectedMeterTypeChoices = ["pv", "battery"];
|
||||
this.selectedMeterTypeChoices = ["pv", "battery"] as MeterType[];
|
||||
this.$nextTick(() => this.meterModal().show());
|
||||
},
|
||||
newAdditionalMeter() {
|
||||
this.selectedMeterId = undefined;
|
||||
this.selectedMeterType = undefined;
|
||||
this.selectedMeterTypeChoices = ["aux", "ext"];
|
||||
this.selectedMeterTypeChoices = ["aux", "ext"] as MeterType[];
|
||||
this.$nextTick(() => this.meterModal().show());
|
||||
},
|
||||
editCharger(id?: number, loadpointType?: LoadpointType) {
|
||||
|
|
@ -912,7 +879,7 @@ export default defineComponent({
|
|||
yamlChanged() {
|
||||
this.loadDirty();
|
||||
},
|
||||
meterAdded(type: SelectedMeterType, name: string) {
|
||||
meterAdded(type: MeterType, name: string) {
|
||||
if (type === "charge") {
|
||||
// update loadpoint
|
||||
(
|
||||
|
|
@ -946,7 +913,7 @@ export default defineComponent({
|
|||
}
|
||||
this.meterChanged();
|
||||
},
|
||||
meterRemoved(type: SelectedMeterType) {
|
||||
meterRemoved(type: MeterType) {
|
||||
if (type === "charge") {
|
||||
// update loadpoint
|
||||
(
|
||||
|
|
|
|||
20
i18n/de.json
20
i18n/de.json
|
|
@ -143,9 +143,9 @@
|
|||
"title": "EEBus"
|
||||
},
|
||||
"ext": {
|
||||
"description": "Kann für Lastmanagement oder Statistikzwecke verwendet werden.",
|
||||
"titleAdd": "Zusätzlichen Zähler hinzufügen",
|
||||
"titleEdit": "Zusätzlichen Zähler bearbeiten"
|
||||
"description": "Erfasst Energiewerte ungeregelter Verbraucher (bspw. Kühlschrank, Waschmaschine, etc.) für Statistikzwecke. Diese Zähler können auch für Lastmanagement verwendet werden (bspw. Unterverteilung).",
|
||||
"titleAdd": "Verbrauchszähler hinzufügen",
|
||||
"titleEdit": "Verbrauchszähler bearbeiten"
|
||||
},
|
||||
"form": {
|
||||
"danger": "Achtung",
|
||||
|
|
@ -196,7 +196,8 @@
|
|||
"title": "HEMS"
|
||||
},
|
||||
"icon": {
|
||||
"change": "ändern"
|
||||
"change": "ändern",
|
||||
"label": "Symbol"
|
||||
},
|
||||
"influx": {
|
||||
"description": "Schreibt Ladedaten und andere Metriken in InfluxDB. Verwende Grafana oder andere Tools, um die Daten zu visualisieren.",
|
||||
|
|
@ -344,13 +345,22 @@
|
|||
"option": {
|
||||
"aux": "Intelligenten Verbraucher hinzufügen",
|
||||
"battery": "Hausbatterie hinzufügen",
|
||||
"ext": "Zusätzlichen Zähler hinzufügen",
|
||||
"ext": "Regulären Verbraucher hinzufügen",
|
||||
"pv": "PV-Anlage hinzufügen"
|
||||
},
|
||||
"save": "Speichern",
|
||||
"specific": "Spezifische Integrationen",
|
||||
"template": "Hersteller",
|
||||
"titleChoice": "Was möchtest du hinzufügen?",
|
||||
"titleLabel": "Titel",
|
||||
"usage": {
|
||||
"aux": "Intelligenter Verbraucher",
|
||||
"battery": "Batteriezähler",
|
||||
"charge": "Ladegerätzähler",
|
||||
"grid": "Netzzähler",
|
||||
"label": "Verwendung",
|
||||
"pv": "PV-Zähler"
|
||||
},
|
||||
"validateSave": "Überprüfen & speichern"
|
||||
},
|
||||
"modbus": {
|
||||
|
|
|
|||
20
i18n/en.json
20
i18n/en.json
|
|
@ -143,9 +143,9 @@
|
|||
"title": "EEBus"
|
||||
},
|
||||
"ext": {
|
||||
"description": "Can be used for load management or statistics purposes.",
|
||||
"titleAdd": "Add External Meter",
|
||||
"titleEdit": "Edit External Meter"
|
||||
"description": "Records energy values of uncontrolled consumers (e.g. refrigerator, washing machine, etc.) for statistical purposes. These meters can also be used for load management (e.g. sub-distribution).",
|
||||
"titleAdd": "Add Consumer Meter",
|
||||
"titleEdit": "Edit Consumer Meter"
|
||||
},
|
||||
"form": {
|
||||
"danger": "Danger",
|
||||
|
|
@ -196,7 +196,8 @@
|
|||
"title": "HEMS"
|
||||
},
|
||||
"icon": {
|
||||
"change": "change"
|
||||
"change": "change",
|
||||
"label": "Icon"
|
||||
},
|
||||
"influx": {
|
||||
"description": "Writes charging data and other metrics to InfluxDB. Use Grafana or other tools to visualize the data.",
|
||||
|
|
@ -344,13 +345,22 @@
|
|||
"option": {
|
||||
"aux": "Add self-regulating consumer",
|
||||
"battery": "Add battery meter",
|
||||
"ext": "Add external meter",
|
||||
"ext": "Add regular consumer",
|
||||
"pv": "Add solar meter"
|
||||
},
|
||||
"save": "Save",
|
||||
"specific": "Specific integrations",
|
||||
"template": "Manufacturer",
|
||||
"titleChoice": "What Do You Want To Add?",
|
||||
"titleLabel": "Title",
|
||||
"usage": {
|
||||
"aux": "Self-regulating consumer meter",
|
||||
"battery": "Battery meter",
|
||||
"charge": "Charger meter",
|
||||
"grid": "Grid meter",
|
||||
"label": "Usage",
|
||||
"pv": "Solar meter"
|
||||
},
|
||||
"validateSave": "Validate & save"
|
||||
},
|
||||
"modbus": {
|
||||
|
|
|
|||
163
tests/config-ext-meter.spec.ts
Normal file
163
tests/config-ext-meter.spec.ts
Normal file
|
|
@ -0,0 +1,163 @@
|
|||
import { test, expect } from "@playwright/test";
|
||||
import { start, stop, restart, baseUrl } from "./evcc";
|
||||
import {
|
||||
editorClear,
|
||||
editorPaste,
|
||||
enableExperimental,
|
||||
expectModalHidden,
|
||||
expectModalVisible,
|
||||
} from "./utils";
|
||||
|
||||
const CONFIG_GRID_ONLY = "config-grid-only.evcc.yaml";
|
||||
|
||||
test.use({ baseURL: baseUrl() });
|
||||
|
||||
test.beforeEach(async () => {
|
||||
await start(CONFIG_GRID_ONLY);
|
||||
});
|
||||
test.afterEach(async () => {
|
||||
await stop();
|
||||
});
|
||||
|
||||
test.describe("ext meter", async () => {
|
||||
test("template-based ext meter", async ({ page }) => {
|
||||
await page.goto("/#/config");
|
||||
await enableExperimental(page, false);
|
||||
await expect(page.getByTestId("ext")).toHaveCount(0);
|
||||
|
||||
await page.getByRole("button", { name: "Add additional meter" }).click();
|
||||
const meterModal = page.getByTestId("meter-modal");
|
||||
await expectModalVisible(meterModal);
|
||||
await meterModal.getByRole("button", { name: "Add regular consumer" }).click();
|
||||
|
||||
await expect(meterModal.getByLabel("Usage")).toHaveValue("charge");
|
||||
|
||||
await meterModal.getByLabel("Manufacturer").selectOption("cFos PowerBrain Meter");
|
||||
await page.waitForLoadState("networkidle");
|
||||
|
||||
await meterModal.getByLabel("Usage").selectOption("battery");
|
||||
await page.waitForLoadState("networkidle");
|
||||
|
||||
await meterModal.getByLabel("Manufacturer").selectOption("Demo battery");
|
||||
await page.waitForLoadState("networkidle");
|
||||
|
||||
await expect(meterModal.getByLabel("Charge")).toBeVisible();
|
||||
await meterModal.getByLabel("Title").fill("House battery");
|
||||
await meterModal.getByLabel("Charge").fill("75");
|
||||
|
||||
const testResult = meterModal.getByTestId("test-result");
|
||||
await testResult.getByRole("link", { name: "validate" }).click();
|
||||
await expect(testResult).toContainText("Status: successful");
|
||||
await expect(testResult).toContainText("75.0%");
|
||||
|
||||
await meterModal.getByRole("button", { name: "Save" }).click();
|
||||
await expectModalHidden(meterModal);
|
||||
|
||||
await expect(page.getByTestId("ext")).toHaveCount(1);
|
||||
await expect(page.getByTestId("ext")).toContainText("House battery");
|
||||
|
||||
await restart(CONFIG_GRID_ONLY);
|
||||
await page.reload();
|
||||
await expect(page.getByTestId("ext")).toHaveCount(1);
|
||||
|
||||
await page.getByTestId("ext").getByRole("button", { name: "edit" }).click();
|
||||
await expectModalVisible(meterModal);
|
||||
await expect(meterModal.getByLabel("Title")).toHaveValue("House battery");
|
||||
await expect(meterModal.getByLabel("Usage")).toBeDisabled();
|
||||
await expect(meterModal.getByLabel("Manufacturer")).toBeDisabled();
|
||||
|
||||
await meterModal.getByLabel("Charge").clear();
|
||||
await meterModal.getByLabel("Charge").fill("85");
|
||||
await testResult.getByRole("link", { name: "validate" }).click();
|
||||
await expect(testResult).toContainText("85.0%");
|
||||
await meterModal.getByRole("button", { name: "Save" }).click();
|
||||
await expectModalHidden(meterModal);
|
||||
|
||||
await page.getByTestId("ext").getByRole("button", { name: "edit" }).click();
|
||||
await expectModalVisible(meterModal);
|
||||
await expect(meterModal.getByLabel("Charge")).toHaveValue("85");
|
||||
|
||||
await meterModal.getByRole("button", { name: "Delete" }).click();
|
||||
await expectModalHidden(meterModal);
|
||||
await expect(page.getByTestId("ext")).toHaveCount(0);
|
||||
|
||||
await restart(CONFIG_GRID_ONLY);
|
||||
await page.reload();
|
||||
await expect(page.getByTestId("ext")).toHaveCount(0);
|
||||
await expect(page.getByTestId("fatal-error")).not.toBeVisible();
|
||||
});
|
||||
|
||||
test("switch from template to custom ext meter", async ({ page }) => {
|
||||
await page.goto("/#/config");
|
||||
await enableExperimental(page, false);
|
||||
|
||||
await page.getByRole("button", { name: "Add additional meter" }).click();
|
||||
const meterModal = page.getByTestId("meter-modal");
|
||||
await expectModalVisible(meterModal);
|
||||
await meterModal.getByRole("button", { name: "Add regular consumer" }).click();
|
||||
|
||||
await meterModal.getByLabel("Title").fill("Custom ext meter");
|
||||
await meterModal.getByLabel("Usage").selectOption("battery");
|
||||
await meterModal.getByLabel("Manufacturer").selectOption("Demo battery");
|
||||
await meterModal.getByLabel("Charge").fill("50");
|
||||
|
||||
await meterModal.getByLabel("Manufacturer").selectOption("User-defined device");
|
||||
await page.waitForLoadState("networkidle");
|
||||
|
||||
const editor = meterModal.getByTestId("yaml-editor");
|
||||
await expect(editor).toContainText("power: # current power");
|
||||
|
||||
const testResult = meterModal.getByTestId("test-result");
|
||||
await testResult.getByRole("link", { name: "validate" }).click();
|
||||
await expect(testResult).toContainText("Status: successful");
|
||||
await expect(testResult).toContainText("1.0 kW");
|
||||
|
||||
await meterModal.getByRole("button", { name: "Save" }).click();
|
||||
await expectModalHidden(meterModal);
|
||||
|
||||
await expect(page.getByTestId("ext")).toHaveCount(1);
|
||||
await expect(page.getByTestId("ext")).toContainText("Custom ext meter");
|
||||
|
||||
await restart(CONFIG_GRID_ONLY);
|
||||
await page.reload();
|
||||
await expect(page.getByTestId("ext")).toHaveCount(1);
|
||||
|
||||
await page.getByTestId("ext").getByRole("button", { name: "edit" }).click();
|
||||
await expectModalVisible(meterModal);
|
||||
await expect(meterModal.getByLabel("Title")).toHaveValue("Custom ext meter");
|
||||
await expect(meterModal.getByLabel("Usage")).toBeDisabled();
|
||||
await expect(meterModal.getByLabel("Manufacturer")).toHaveValue("User-defined device");
|
||||
|
||||
await page.waitForLoadState("networkidle");
|
||||
await expect(editor).toContainText("value: 1000 # W");
|
||||
|
||||
await editorClear(editor);
|
||||
await editorPaste(
|
||||
editor,
|
||||
page,
|
||||
`power:
|
||||
source: const
|
||||
value: 2000 # W`
|
||||
);
|
||||
|
||||
await testResult.getByRole("link", { name: "validate" }).click();
|
||||
await expect(testResult).toContainText("Status: successful");
|
||||
await expect(testResult).toContainText("2.0 kW");
|
||||
|
||||
await meterModal.getByRole("button", { name: "Save" }).click();
|
||||
await expectModalHidden(meterModal);
|
||||
|
||||
await page.getByTestId("ext").getByRole("button", { name: "edit" }).click();
|
||||
await expectModalVisible(meterModal);
|
||||
await page.waitForLoadState("networkidle");
|
||||
await expect(editor).toContainText("value: 2000 # W");
|
||||
|
||||
await meterModal.getByRole("button", { name: "Delete" }).click();
|
||||
await expectModalHidden(meterModal);
|
||||
await expect(page.getByTestId("ext")).toHaveCount(0);
|
||||
|
||||
await restart(CONFIG_GRID_ONLY);
|
||||
await page.reload();
|
||||
await expect(page.getByTestId("ext")).toHaveCount(0);
|
||||
});
|
||||
});
|
||||
Loading…
Add table
Add a link
Reference in a new issue