Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 18 additions & 0 deletions src/components/devices/enecess/ecomain/counter.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
<template>
<div class="device-enecess-ecomain-counter">
<openwb-base-heading> Einstellungen für EcoMain EVU-Zähler </openwb-base-heading>
<openwb-base-alert subtype="info">
Die Phasenwerte und Gesamtwerte werden vom Hauptanschluss des EcoMain übernommen. Eine Frequenz wird vom EcoMain
nicht bereitgestellt.
</openwb-base-alert>
</div>
</template>

<script>
import ComponentConfigMixin from "../../ComponentConfigMixin.vue";

export default {
name: "DeviceEneCessEcoMainCounter",
mixins: [ComponentConfigMixin],
};
</script>
28 changes: 28 additions & 0 deletions src/components/devices/enecess/ecomain/device.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
<template>
<div class="device-enecess-ecomain">
<openwb-base-heading> Einstellungen für enecess EcoMain </openwb-base-heading>
<openwb-base-alert subtype="info"> Softwareversion 138 oder höher ist erforderlich. </openwb-base-alert>
<openwb-base-text-input
title="IP-Adresse / Hostname"
subtype="host"
required
:model-value="device.configuration.ip_address"
@update:model-value="updateConfiguration($event, 'configuration.ip_address')"
/>
<openwb-base-text-input
title="Seriennummer"
required
:model-value="device.configuration.serial_number"
@update:model-value="updateConfiguration($event, 'configuration.serial_number')"
/>
</div>
</template>

<script>
import DeviceConfigMixin from "../../DeviceConfigMixin.vue";

export default {
name: "DeviceEneCessEcoMain",
mixins: [DeviceConfigMixin],
};
</script>
152 changes: 152 additions & 0 deletions src/components/devices/enecess/ecomain/inverter.spec.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,152 @@
import { describe, expect, it } from "vitest";
import { mount } from "@vue/test-utils";

import Inverter from "./inverter.vue";

const baseChannels = [
{ phase: 1, source: 0, channel: 1 },
{ phase: 2, source: 0, channel: 2 },
{ phase: 3, source: 0, channel: 3 },
];

function mountInverter(configuration) {
return mount(Inverter, {
props: {
device: { configuration: {} },
component: { configuration },
},
global: {
stubs: {
OpenwbBaseAlert: true,
OpenwbBaseCheckboxInput: true,
OpenwbBaseHeading: true,
OpenwbBaseNumberInput: true,
OpenwbBaseSelectInput: {
name: "OpenwbBaseSelectInput",
props: ["title", "required", "options", "modelValue"],
template: "<div />",
},
},
},
});
}

describe("EcoMain inverter settings", () => {
it("uses numeric channel dropdown options and emits numeric channel updates", async () => {
const wrapper = mountInverter({
phase_count: 1,
invert: false,
channels: [{ phase: 1, source: 0, channel: 1 }],
});

const channelEditor = wrapper
.findAllComponents({ name: "OpenwbBaseSelectInput" })
.find((editor) => editor.props("title") === "Kanal");

expect(wrapper.findComponent({ name: "OpenwbBaseNumberInput" }).exists()).toBe(false);
expect(channelEditor.props("options")).toEqual(
Array.from({ length: 10 }, (_, index) => ({ value: index + 1, text: String(index + 1) })),
);

await channelEditor.vm.$emit("update:model-value", 10);

expect(wrapper.emitted("update:configuration")).toContainEqual([
{ value: [{ phase: 1, source: 0, channel: 10 }], object: "configuration.channels" },
]);
});

it("adds unique phases and physical channels without mutating the configured channel", () => {
const configuration = {
phase_count: 1,
invert: false,
channels: [{ phase: 3, source: 0, channel: 2 }],
};
const wrapper = mountInverter(configuration);

wrapper.vm.changePhaseCount(3);

const updates = wrapper.emitted("update:configuration").map(([update]) => update);
expect(updates[0]).toEqual({ value: 3, object: "configuration.phase_count" });
expect(updates[1].object).toBe("configuration.channels");

const channels = updates[1].value;
expect(channels).toHaveLength(3);
expect(channels[0]).toEqual({ phase: 3, source: 0, channel: 2 });
expect(new Set(channels.map(({ phase }) => phase))).toEqual(new Set([1, 2, 3]));
expect(new Set(channels.map(({ source, channel }) => `${source}:${channel}`)).size).toBe(3);
expect(configuration.channels).toEqual([{ phase: 3, source: 0, channel: 2 }]);
});

it("keeps only the first channel when switching to single phase", () => {
const configuration = {
phase_count: 3,
invert: false,
channels: baseChannels,
};
const wrapper = mountInverter(configuration);

wrapper.vm.changePhaseCount(1);

const updates = wrapper.emitted("update:configuration").map(([update]) => update);
expect(updates).toEqual([
{ value: 1, object: "configuration.phase_count" },
{ value: [{ phase: 1, source: 0, channel: 1 }], object: "configuration.channels" },
]);
expect(configuration.channels).toEqual(baseChannels);
});

it.each([
[
"accepts a valid single-phase configuration",
{ phase_count: 1, invert: false, channels: [{ phase: 1, source: 0, channel: 1 }] },
"",
],
[
"rejects an unsupported phase count",
{ phase_count: 2, invert: false, channels: baseChannels.slice(0, 2) },
"Die Phasenanzahl muss 1 oder 3 sein.",
],
[
"rejects a channel count that differs from the phase count",
{ phase_count: 3, invert: false, channels: baseChannels.slice(0, 2) },
"Die Anzahl der EcoMain-Kanäle stimmt nicht mit der Phasenanzahl überein.",
],
[
"rejects an invalid phase",
{ phase_count: 1, invert: false, channels: [{ phase: 4, source: 0, channel: 1 }] },
"Die Phase muss L1, L2 oder L3 sein.",
],
[
"rejects an invalid source",
{ phase_count: 1, invert: false, channels: [{ phase: 1, source: 4, channel: 1 }] },
"Die Quelle muss Hauptgerät oder Slave 1 bis 3 sein.",
],
[
"rejects channel 11",
{ phase_count: 1, invert: false, channels: [{ phase: 1, source: 0, channel: 11 }] },
"Der EcoMain-Kanal muss zwischen 1 und 10 liegen.",
],
[
"rejects duplicate phases",
{
phase_count: 3,
invert: false,
channels: [baseChannels[0], { ...baseChannels[1], phase: 1 }, baseChannels[2]],
},
"Bei dreiphasiger Messung müssen L1, L2 und L3 jeweils einmal konfiguriert sein.",
],
[
"rejects duplicate physical channels",
{
phase_count: 3,
invert: false,
channels: [baseChannels[0], { phase: 2, source: 0, channel: 1 }, baseChannels[2]],
},
"Eine EcoMain-Quelle und ein Kanal dürfen nicht mehrfach verwendet werden.",
],
])("%s", (_name, configuration, expectedError) => {
const wrapper = mountInverter(configuration);

expect(wrapper.vm.validationError).toBe(expectedError);
});
});
138 changes: 138 additions & 0 deletions src/components/devices/enecess/ecomain/inverter.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,138 @@
<template>
<div class="device-enecess-ecomain-inverter">
<openwb-base-heading> Einstellungen für EcoMain Wechselrichter </openwb-base-heading>
<openwb-base-select-input
title="Phasenanzahl"
required
:options="[
{ value: 1, text: 'Einphasig' },
{ value: 3, text: 'Dreiphasig' },
]"
:model-value="component.configuration.phase_count"
@update:model-value="changePhaseCount"
/>
<openwb-base-checkbox-input
title="Messrichtung invertieren"
:model-value="component.configuration.invert"
@update:model-value="updateConfiguration($event, 'configuration.invert')"
>
<template #help>
Aktivieren, wenn die Stromwandler entgegen der vorgesehenen Messrichtung montiert wurden.
</template>
</openwb-base-checkbox-input>
<template
v-for="(channel, index) in component.configuration.channels"
:key="index"
>
<openwb-base-heading> Kanal {{ index + 1 }} </openwb-base-heading>
<openwb-base-select-input
title="Phase"
required
:options="[
{ value: 1, text: 'L1' },
{ value: 2, text: 'L2' },
{ value: 3, text: 'L3' },
]"
:model-value="channel.phase"
@update:model-value="updateChannel(index, 'phase', $event)"
/>
<openwb-base-select-input
title="Quelle"
required
:options="[
{ value: 0, text: 'Hauptgerät' },
{ value: 1, text: 'Slave 1' },
{ value: 2, text: 'Slave 2' },
{ value: 3, text: 'Slave 3' },
]"
:model-value="channel.source"
@update:model-value="updateChannel(index, 'source', $event)"
/>
<openwb-base-select-input
title="Kanal"
required
:options="channelOptions"
:model-value="channel.channel"
@update:model-value="updateChannel(index, 'channel', $event)"
/>
</template>
<openwb-base-alert
v-if="validationError"
subtype="danger"
>
{{ validationError }}
</openwb-base-alert>
</div>
</template>

<script>
import ComponentConfigMixin from "../../ComponentConfigMixin.vue";

export default {
name: "DeviceEneCessEcoMainInverter",
mixins: [ComponentConfigMixin],
computed: {
channelOptions() {
return Array.from({ length: 10 }, (_, index) => {
const value = index + 1;
return { value, text: String(value) };
});
},
validationError() {
const configuration = this.component.configuration;
if (![1, 3].includes(configuration.phase_count)) {
return "Die Phasenanzahl muss 1 oder 3 sein.";
}
if (configuration.channels.length !== configuration.phase_count) {
return "Die Anzahl der EcoMain-Kanäle stimmt nicht mit der Phasenanzahl überein.";
}
for (const channel of configuration.channels) {
if (![1, 2, 3].includes(channel.phase)) {
return "Die Phase muss L1, L2 oder L3 sein.";
}
if (![0, 1, 2, 3].includes(channel.source)) {
return "Die Quelle muss Hauptgerät oder Slave 1 bis 3 sein.";
}
if (channel.channel < 1 || channel.channel > 10) {
return "Der EcoMain-Kanal muss zwischen 1 und 10 liegen.";
}
}
if (configuration.phase_count === 3 && new Set(configuration.channels.map(({ phase }) => phase)).size !== 3) {
return "Bei dreiphasiger Messung müssen L1, L2 und L3 jeweils einmal konfiguriert sein.";
}
const physicalChannels = new Set(configuration.channels.map(({ source, channel }) => `${source}:${channel}`));
if (physicalChannels.size !== configuration.channels.length) {
return "Eine EcoMain-Quelle und ein Kanal dürfen nicht mehrfach verwendet werden.";
}
return "";
},
},
methods: {
updateChannel(index, key, value) {
const channels = this.component.configuration.channels.map((item) => ({ ...item }));
channels[index] = { ...channels[index], [key]: value };
this.updateConfiguration(channels, "configuration.channels");
},
changePhaseCount(value) {
const current = this.component.configuration.channels.map((item) => ({ ...item }));
let channels;
if (value === 1) {
channels = [current[0] || { phase: 1, source: 0, channel: 1 }];
} else {
const first = current[0] || { phase: 1, source: 0, channel: 1 };
const missingPhases = [1, 2, 3].filter((phase) => phase !== first.phase);
const used = new Set([`${first.source}:${first.channel}`]);
channels = [first];
for (const phase of missingPhases) {
let channel = 1;
while (used.has(`0:${channel}`)) channel += 1;
channels.push({ phase, source: 0, channel });
used.add(`0:${channel}`);
}
}
this.updateConfiguration(value, "configuration.phase_count");
this.updateConfiguration(channels, "configuration.channels");
},
},
};
</script>
Loading