Config UI: manually convert additional meter to consumer (#31130)
This commit is contained in:
parent
31e93f1dfc
commit
4c58f6b69f
10 changed files with 159 additions and 3 deletions
|
|
@ -8,6 +8,8 @@
|
|||
@test="$emit('test')"
|
||||
/>
|
||||
|
||||
<slot name="after-test"></slot>
|
||||
|
||||
<div class="mt-4 d-flex justify-content-between">
|
||||
<button
|
||||
v-if="isDeletable"
|
||||
|
|
|
|||
|
|
@ -178,7 +178,11 @@
|
|||
@save="handleSave"
|
||||
@remove="handleRemove"
|
||||
@test="testManually"
|
||||
/>
|
||||
>
|
||||
<template #after-test>
|
||||
<slot name="after-test" :values="values"></slot>
|
||||
</template>
|
||||
</DeviceModalActions>
|
||||
</template>
|
||||
</form>
|
||||
</GenericModal>
|
||||
|
|
|
|||
|
|
@ -85,6 +85,18 @@
|
|||
/>
|
||||
</FormRow>
|
||||
</template>
|
||||
|
||||
<template #after-test>
|
||||
<button
|
||||
v-if="canConvertToConsumer"
|
||||
type="button"
|
||||
class="btn btn-link text-muted"
|
||||
tabindex="0"
|
||||
@click="convertToConsumer"
|
||||
>
|
||||
{{ $t("config.meter.convertToConsumer.link") }}
|
||||
</button>
|
||||
</template>
|
||||
</DeviceModalBase>
|
||||
</template>
|
||||
|
||||
|
|
@ -105,7 +117,7 @@ import {
|
|||
} from "./DeviceModal";
|
||||
import { customTemplateOption, type TemplateGroup } from "./DeviceModal/TemplateSelector.vue";
|
||||
import defaultMeterYaml from "./defaultYaml/meter.yaml?raw";
|
||||
import { getModal, replaceModal } from "@/configModal";
|
||||
import { getModal, replaceModal, closeModal } from "@/configModal";
|
||||
|
||||
const initialValues = {
|
||||
type: ConfigType.Template,
|
||||
|
|
@ -192,6 +204,9 @@ export default defineComponent({
|
|||
isNew(): boolean {
|
||||
return this.id === undefined;
|
||||
},
|
||||
canConvertToConsumer(): boolean {
|
||||
return !this.isNew && this.selectedType === "ext" && this.extMeterUsage === "charge";
|
||||
},
|
||||
extMeterUsageOptions() {
|
||||
return ["charge", "aux", "grid", "pv", "battery"].map((key) => ({
|
||||
name: this.$t(`config.meter.usage.${key}`),
|
||||
|
|
@ -270,6 +285,11 @@ export default defineComponent({
|
|||
const result = { action, name, type };
|
||||
this.$emit("changed", result);
|
||||
},
|
||||
async convertToConsumer() {
|
||||
if (!window.confirm(this.$t("config.meter.convertToConsumer.confirm"))) return;
|
||||
this.$emit("changed", { action: "converted", type: "ext", id: this.id });
|
||||
await closeModal();
|
||||
},
|
||||
handleClose() {
|
||||
this.extMeterUsage = "charge";
|
||||
this.$emit("close");
|
||||
|
|
|
|||
|
|
@ -11,7 +11,7 @@ export interface ModalEntry {
|
|||
}
|
||||
|
||||
export interface ModalResult {
|
||||
action: "added" | "updated" | "removed" | "cancelled";
|
||||
action: "added" | "updated" | "removed" | "converted" | "cancelled";
|
||||
name?: string;
|
||||
id?: number;
|
||||
type?: string;
|
||||
|
|
|
|||
|
|
@ -1097,6 +1097,17 @@ export default defineComponent({
|
|||
}
|
||||
}
|
||||
|
||||
// Converted: move ext meter to consumers (history is reconciled on restart)
|
||||
if (result.action === "converted") {
|
||||
const name = this.meters.find((m) => m.id === result.id)?.name;
|
||||
if (name) {
|
||||
const ext = (this.site.ext || []).filter((n) => n !== name);
|
||||
const consumers = [...(this.site.consumers || []), name];
|
||||
await api.put("/config/site", { ext, consumers });
|
||||
await this.loadSite();
|
||||
}
|
||||
}
|
||||
|
||||
// Removed: reload site config
|
||||
if (result.action === "removed") {
|
||||
await this.loadSite();
|
||||
|
|
|
|||
|
|
@ -41,6 +41,14 @@ func NewCollector(group, name, title string, opt ...func(*Accumulator)) (*Collec
|
|||
|
||||
// createEntity ensures the entity row exists and refreshes its title.
|
||||
func createEntity(group, name, title string) (entity, error) {
|
||||
// keep history when a meter is regrouped "meter" -> "consumer" (aux, ext convert)
|
||||
if group == Consumer {
|
||||
var prev entity
|
||||
if db.Instance.Where(`"group" = ? AND name = ?`, Meter, name).Limit(1).Find(&prev).RowsAffected > 0 {
|
||||
db.Instance.Model(&prev).UpdateColumn("group", Consumer)
|
||||
}
|
||||
}
|
||||
|
||||
e := entity{Group: group, Name: name}
|
||||
|
||||
if err := db.Instance.Where(&e).Attrs(entity{Title: title}).FirstOrCreate(&e).Error; err != nil {
|
||||
|
|
|
|||
|
|
@ -309,3 +309,56 @@ func TestCreateEntityRefreshesTitle(t *testing.T) {
|
|||
require.NoError(t, db.Instance.Model(new(entity)).Where("\"group\" = ? AND name = ?", "grid", "grid").Count(&count).Error)
|
||||
require.EqualValues(t, 1, count, "must not duplicate existing rows")
|
||||
}
|
||||
|
||||
// a meter regrouped to consumer keeps its id and history via in-place relabel
|
||||
func TestCreateEntityReconcilesExtToConsumer(t *testing.T) {
|
||||
require.NoError(t, db.NewInstance("sqlite", ":memory:"))
|
||||
require.NoError(t, SetupSchema())
|
||||
|
||||
// ext meter with a persisted history slot
|
||||
ext, err := createEntity(Meter, "db:5", "Fridge")
|
||||
require.NoError(t, err)
|
||||
require.NoError(t, persist(ext, time.Unix(15*60, 0), 0.3, 0))
|
||||
|
||||
// reconfigured as consumer: same row relabeled, history intact
|
||||
con, err := createEntity(Consumer, "db:5", "Fridge")
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, ext.Id, con.Id, "must reuse the existing row")
|
||||
require.Equal(t, Consumer, con.Group)
|
||||
|
||||
var stored entity
|
||||
require.NoError(t, db.Instance.First(&stored, ext.Id).Error)
|
||||
require.Equal(t, Consumer, stored.Group, "group must be persisted")
|
||||
|
||||
// no duplicate entity for the name
|
||||
var entities int64
|
||||
require.NoError(t, db.Instance.Model(new(entity)).Where("name = ?", "db:5").Count(&entities).Error)
|
||||
require.EqualValues(t, 1, entities)
|
||||
|
||||
// history row still attached to the (now consumer) entity
|
||||
var meters int64
|
||||
require.NoError(t, db.Instance.Model(new(meter)).Where("meter = ?", ext.Id).Count(&meters).Error)
|
||||
require.EqualValues(t, 1, meters)
|
||||
}
|
||||
|
||||
// an existing consumer row blocks the relabel, leaving the meter row untouched
|
||||
func TestCreateEntityReconcileGuard(t *testing.T) {
|
||||
require.NoError(t, db.NewInstance("sqlite", ":memory:"))
|
||||
require.NoError(t, SetupSchema())
|
||||
|
||||
meterRow, err := createEntity(Meter, "db:7", "")
|
||||
require.NoError(t, err)
|
||||
|
||||
// pre-existing consumer row blocks the in-place relabel
|
||||
conRow := entity{Group: Consumer, Name: "db:7"}
|
||||
require.NoError(t, db.Instance.Create(&conRow).Error)
|
||||
|
||||
got, err := createEntity(Consumer, "db:7", "")
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, conRow.Id, got.Id, "must reuse existing consumer row")
|
||||
|
||||
// meter row untouched
|
||||
var stored entity
|
||||
require.NoError(t, db.Instance.First(&stored, meterRow.Id).Error)
|
||||
require.Equal(t, Meter, stored.Group)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -535,6 +535,10 @@
|
|||
},
|
||||
"meter": {
|
||||
"cancel": "Abbrechen",
|
||||
"convertToConsumer": {
|
||||
"confirm": "Wandelt diesen \"zusätzlichen Zähler\" in einen \"Verbraucher\" um.",
|
||||
"link": "In Verbraucher umwandeln"
|
||||
},
|
||||
"delete": "Löschen",
|
||||
"generic": "Generische Integrationen",
|
||||
"option": {
|
||||
|
|
|
|||
|
|
@ -534,6 +534,10 @@
|
|||
},
|
||||
"meter": {
|
||||
"cancel": "Cancel",
|
||||
"convertToConsumer": {
|
||||
"confirm": "Converts this \"additional meter\" to a \"consumer\".",
|
||||
"link": "Convert to consumer"
|
||||
},
|
||||
"delete": "Delete",
|
||||
"generic": "Generic integrations",
|
||||
"option": {
|
||||
|
|
|
|||
|
|
@ -86,6 +86,56 @@ test.describe("ext meter", async () => {
|
|||
await expect(page.getByTestId("fatal-error")).not.toBeVisible();
|
||||
});
|
||||
|
||||
test("convert charge ext meter to consumer", async ({ page }) => {
|
||||
await page.goto("/#/config");
|
||||
await expect(page.getByTestId("ext")).toHaveCount(0);
|
||||
await expect(page.getByTestId("consumer")).toHaveCount(0);
|
||||
|
||||
// additional meter defaults to usage charge
|
||||
await createAdditionalMeter(page, "Fridge", "150");
|
||||
await expect(page.getByTestId("ext")).toHaveCount(1);
|
||||
|
||||
const meterModal = page.getByTestId("meter-modal");
|
||||
await page.getByTestId("ext").getByRole("button", { name: "edit" }).click();
|
||||
await expectModalVisible(meterModal);
|
||||
|
||||
page.once("dialog", (dialog) => dialog.accept());
|
||||
await meterModal.getByRole("button", { name: "Convert to consumer" }).click();
|
||||
await expectModalHidden(meterModal);
|
||||
|
||||
// moved from additional meters into consumers
|
||||
await expect(page.getByTestId("ext")).toHaveCount(0);
|
||||
await expect(page.getByTestId("consumer")).toHaveCount(1);
|
||||
await expect(page.getByTestId("consumer")).toContainText("Fridge");
|
||||
|
||||
// persists across restart (history reconciled on boot)
|
||||
await restart(CONFIG_GRID_ONLY);
|
||||
await page.reload();
|
||||
await expect(page.getByTestId("ext")).toHaveCount(0);
|
||||
await expect(page.getByTestId("consumer")).toHaveCount(1);
|
||||
await expect(page.getByTestId("consumer")).toContainText("Fridge");
|
||||
await expect(page.getByTestId("fatal-error")).not.toBeVisible();
|
||||
});
|
||||
|
||||
test("convert option hidden for non-charge ext meter", async ({ page }) => {
|
||||
await page.goto("/#/config");
|
||||
|
||||
await page.getByRole("button", { name: "Add additional meter" }).click();
|
||||
const meterModal = page.getByTestId("meter-modal");
|
||||
await expectModalVisible(meterModal);
|
||||
|
||||
await meterModal.getByLabel("Usage").selectOption("battery");
|
||||
await meterModal.getByLabel("Manufacturer").selectOption("Demo battery");
|
||||
await meterModal.getByLabel("Title").fill("House battery");
|
||||
await meterModal.getByLabel("Charge").fill("75");
|
||||
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.getByRole("button", { name: "Convert to consumer" })).toHaveCount(0);
|
||||
});
|
||||
|
||||
test("switch from template to custom ext meter", async ({ page }) => {
|
||||
await page.goto("/#/config");
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue