History: fix axis, scale and units (#30042)
This commit is contained in:
parent
29a932ce21
commit
41c169ee9a
4 changed files with 308 additions and 92 deletions
|
|
@ -1,5 +1,5 @@
|
|||
<template>
|
||||
<div class="history-chart-wrapper">
|
||||
<div class="history-chart-wrapper" :data-testid="`group-chart-${group}`">
|
||||
<div ref="chartEl" class="history-chart"></div>
|
||||
</div>
|
||||
</template>
|
||||
|
|
@ -58,6 +58,18 @@ export function alphaColor(color: string, alpha: number): string {
|
|||
return c;
|
||||
}
|
||||
|
||||
// Symmetric axis regardless of whether the period contains both directions.
|
||||
const BIDIRECTIONAL_GROUPS: ReadonlySet<string> = new Set(["grid", "battery"]);
|
||||
|
||||
// Round up to a nice number (5-tick symmetric axis: -L, -L/2, 0, L/2, L).
|
||||
function niceCeil(v: number): number {
|
||||
if (v <= 0) return 0;
|
||||
const mag = Math.pow(10, Math.floor(Math.log10(v)));
|
||||
const r = v / mag;
|
||||
const n = r <= 1 ? 1 : r <= 2 ? 2 : r <= 3 ? 3 : r <= 4 ? 4 : r <= 6 ? 6 : r <= 8 ? 8 : 10;
|
||||
return n * mag;
|
||||
}
|
||||
|
||||
export default defineComponent({
|
||||
name: "GroupChart",
|
||||
mixins: [formatter],
|
||||
|
|
@ -97,9 +109,22 @@ export default defineComponent({
|
|||
valueFactor(): number {
|
||||
return this.period === PERIODS.DAY ? 4 : 1;
|
||||
},
|
||||
// Peak per-slot stacked value in the display unit (kW for day, kWh otherwise).
|
||||
// Raw peak in display unit. Bidirectional: max of |either direction|.
|
||||
// Unidirectional: max of stacked per-slot sum.
|
||||
axisPeak(): number {
|
||||
const factor = this.valueFactor;
|
||||
let max = 0;
|
||||
if (this.isBidirectional) {
|
||||
for (const s of this.visibleSeries) {
|
||||
for (const slot of s.data) {
|
||||
const a = Math.abs(slot.energy) * factor;
|
||||
const b = Math.abs(slot.returnEnergy) * factor;
|
||||
if (a > max) max = a;
|
||||
if (b > max) max = b;
|
||||
}
|
||||
}
|
||||
return max;
|
||||
}
|
||||
const slotSums = new Map<string, number>();
|
||||
for (const s of this.visibleSeries) {
|
||||
for (const slot of s.data) {
|
||||
|
|
@ -107,25 +132,26 @@ export default defineComponent({
|
|||
slotSums.set(slot.start, (slotSums.get(slot.start) || 0) + v);
|
||||
}
|
||||
}
|
||||
let max = 0;
|
||||
for (const v of slotSums.values()) if (v > max) max = v;
|
||||
return max;
|
||||
},
|
||||
maxVisibleKw(): number {
|
||||
return this.period === PERIODS.DAY ? this.axisPeak : Infinity;
|
||||
// W/Wh scale when peak below 1 kW(h). Zero data falls here too.
|
||||
useSmallUnit(): boolean {
|
||||
return this.axisPeak < 1;
|
||||
},
|
||||
// Switch to watts when the peak is below 1 kW so small values stay readable.
|
||||
useWatts(): boolean {
|
||||
return this.period === PERIODS.DAY && this.maxVisibleKw < 1;
|
||||
// Rounded range. W mode floors at 1 kW (= 1000 W) for stable context.
|
||||
axisLimit(): number {
|
||||
const v = niceCeil(this.axisPeak);
|
||||
return this.useSmallUnit ? Math.max(v, 1) : v;
|
||||
},
|
||||
// 1 decimal when echarts may pick 0.5-step splits, else 0 — avoids "1, 1, 2, 2".
|
||||
// 1-3 kW(h) band gets one decimal to avoid duplicate integer ticks.
|
||||
axisDigits(): number {
|
||||
if (this.isBidirectional || this.useWatts) return 0;
|
||||
return this.axisPeak < 3 ? 1 : 0;
|
||||
if (this.useSmallUnit) return 0;
|
||||
return this.axisLimit > 0 && this.axisLimit <= 3 ? 1 : 0;
|
||||
},
|
||||
unit(): "W" | "kW" | "kWh" {
|
||||
if (this.period !== PERIODS.DAY) return "kWh";
|
||||
return this.useWatts ? "W" : "kW";
|
||||
unit(): "W" | "Wh" | "kW" | "kWh" {
|
||||
if (this.period === PERIODS.DAY) return this.useSmallUnit ? "W" : "kW";
|
||||
return this.useSmallUnit ? "Wh" : "kWh";
|
||||
},
|
||||
// Consumer groups have many palette colours per entity — a single neutral
|
||||
// tooltip background reads better than picking one entity's colour.
|
||||
|
|
@ -141,40 +167,7 @@ export default defineComponent({
|
|||
return this.series.filter((s, i) => (s.paletteIndex ?? i) === idx);
|
||||
},
|
||||
isBidirectional(): boolean {
|
||||
for (const s of this.visibleSeries) {
|
||||
let energy = 0;
|
||||
let returnEnergy = 0;
|
||||
for (const slot of s.data) {
|
||||
energy += slot.energy;
|
||||
returnEnergy += slot.returnEnergy;
|
||||
}
|
||||
if (energy > 0 && returnEnergy > 0) return true;
|
||||
}
|
||||
return false;
|
||||
},
|
||||
// Soft symmetric limit. Rounded up to an even-friendly nice number so
|
||||
// splitNumber: 4 gives integer ticks ±X, ±X/2, 0.
|
||||
bidirectionalLimit(): number {
|
||||
let m = 0;
|
||||
const factor = this.valueFactor;
|
||||
for (const s of this.visibleSeries) {
|
||||
for (const slot of s.data) {
|
||||
const a = Math.abs(slot.energy * factor);
|
||||
const b = Math.abs(slot.returnEnergy * factor);
|
||||
if (a > m) m = a;
|
||||
if (b > m) m = b;
|
||||
}
|
||||
}
|
||||
if (m <= 0) return 2;
|
||||
const mag = Math.pow(10, Math.floor(Math.log10(m)));
|
||||
const r = m / mag;
|
||||
let n;
|
||||
if (r <= 2) n = 2;
|
||||
else if (r <= 4) n = 4;
|
||||
else if (r <= 6) n = 6;
|
||||
else if (r <= 8) n = 8;
|
||||
else n = 10;
|
||||
return n * mag;
|
||||
return BIDIRECTIONAL_GROUPS.has(this.group);
|
||||
},
|
||||
categoryTimestamps(): number[] {
|
||||
const out: number[] = [];
|
||||
|
|
@ -576,13 +569,15 @@ export default defineComponent({
|
|||
},
|
||||
},
|
||||
yAxis: forecastYAxis({
|
||||
...(this.isBidirectional
|
||||
...(this.isBidirectional && this.axisLimit > 0
|
||||
? {
|
||||
min: -this.bidirectionalLimit,
|
||||
max: this.bidirectionalLimit,
|
||||
interval: this.bidirectionalLimit / 2,
|
||||
min: -this.axisLimit,
|
||||
max: this.axisLimit,
|
||||
interval: this.axisLimit / 2,
|
||||
}
|
||||
: {}),
|
||||
: this.useSmallUnit
|
||||
? { max: this.axisLimit, interval: this.axisLimit / 4 }
|
||||
: {}),
|
||||
position: "right",
|
||||
splitNumber: 3,
|
||||
splitLine: {
|
||||
|
|
@ -607,15 +602,12 @@ export default defineComponent({
|
|||
axisLabel: {
|
||||
color: colors.muted || "",
|
||||
hideOverlap: true,
|
||||
formatter: (v: number) =>
|
||||
this.period === PERIODS.DAY
|
||||
? this.fmtW(
|
||||
v * 1000,
|
||||
this.useWatts ? POWER_UNIT.W : POWER_UNIT.KW,
|
||||
false,
|
||||
this.axisDigits
|
||||
)
|
||||
: this.fmtWh(v * 1000, POWER_UNIT.KW, false, this.axisDigits),
|
||||
formatter: (v: number): string => {
|
||||
const unit = this.useSmallUnit ? POWER_UNIT.W : POWER_UNIT.KW;
|
||||
return this.period === PERIODS.DAY
|
||||
? this.fmtW(v * 1000, unit, false, this.axisDigits)
|
||||
: this.fmtWh(v * 1000, unit, false, this.axisDigits);
|
||||
},
|
||||
},
|
||||
}),
|
||||
series: this.echartsSeries,
|
||||
|
|
@ -647,7 +639,7 @@ export default defineComponent({
|
|||
yAxis: opt["yAxis"],
|
||||
series: opt["series"],
|
||||
},
|
||||
fullReset ? { notMerge: true } : { replaceMerge: ["series"] }
|
||||
fullReset ? { notMerge: true } : { replaceMerge: ["series", "yAxis"] }
|
||||
);
|
||||
this.previousFocusedEntity = this.focusedEntity as number | null;
|
||||
this.previousPeriod = this.period as PERIODS;
|
||||
|
|
|
|||
|
|
@ -284,23 +284,13 @@ export default defineComponent({
|
|||
},
|
||||
visibleGroups(): string[] {
|
||||
return GROUP_ORDER.filter((g) => {
|
||||
// Consumption uses `home` as the source of truth, so the section
|
||||
// shows up whenever home has data, even without explicit meters.
|
||||
// Consumption section follows `home` (the source of truth).
|
||||
if (g === "meter") {
|
||||
const home = this.seriesByGroup["home"];
|
||||
if (
|
||||
home?.some((s) =>
|
||||
s.data.some((slot) => slot.energy !== 0 || slot.returnEnergy !== 0)
|
||||
)
|
||||
) {
|
||||
return true;
|
||||
}
|
||||
if (home?.some((s) => s.data.length > 0)) return true;
|
||||
}
|
||||
const list = this.seriesByGroup[g];
|
||||
if (!list?.length) return false;
|
||||
return list.some((s) =>
|
||||
s.data.some((slot) => slot.energy !== 0 || slot.returnEnergy !== 0)
|
||||
);
|
||||
return !!list?.some((s) => s.data.length > 0);
|
||||
});
|
||||
},
|
||||
// Series shown in the chart. For the consumption (`meter`) group we append
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
import { test, expect } from "@playwright/test";
|
||||
import { test, expect, type Page, type Locator } from "@playwright/test";
|
||||
import { start, stop, baseUrl } from "./evcc";
|
||||
|
||||
test.use({ baseURL: baseUrl() });
|
||||
|
|
@ -13,7 +13,7 @@ test.afterAll(async () => {
|
|||
await stop();
|
||||
});
|
||||
|
||||
test.describe("energy history API", () => {
|
||||
test.describe("api", () => {
|
||||
test("15-minute resolution", async ({ request }) => {
|
||||
const res = await request.get(`${baseUrl()}/api/history/energy`, {
|
||||
params: { from, to },
|
||||
|
|
@ -23,8 +23,8 @@ test.describe("energy history API", () => {
|
|||
const data = await res.json();
|
||||
expect(data).toHaveLength(2);
|
||||
|
||||
const grid = data.find((s: any) => s.name === "grid");
|
||||
const home = data.find((s: any) => s.name === "home");
|
||||
const grid = data.find((s: { name: string }) => s.name === "grid");
|
||||
const home = data.find((s: { name: string }) => s.name === "home");
|
||||
expect(grid).toBeDefined();
|
||||
expect(home).toBeDefined();
|
||||
|
||||
|
|
@ -46,8 +46,8 @@ test.describe("energy history API", () => {
|
|||
const data = await res.json();
|
||||
expect(data).toHaveLength(2);
|
||||
|
||||
const grid = data.find((s: any) => s.name === "grid");
|
||||
const home = data.find((s: any) => s.name === "home");
|
||||
const grid = data.find((s: { name: string }) => s.name === "grid");
|
||||
const home = data.find((s: { name: string }) => s.name === "home");
|
||||
expect(grid).toBeDefined();
|
||||
expect(home).toBeDefined();
|
||||
|
||||
|
|
@ -70,3 +70,135 @@ test.describe("energy history API", () => {
|
|||
expect(gridDay2.returnEnergy).toBeCloseTo(0.2, 4);
|
||||
});
|
||||
});
|
||||
|
||||
async function gotoDay(page: Page, year: number, month: number, day: number): Promise<void> {
|
||||
await page.goto(`/#/history?period=day&year=${year}&month=${month}&day=${day}`);
|
||||
await expect(page.getByRole("heading", { name: /history/i }).first()).toBeVisible();
|
||||
}
|
||||
|
||||
async function gotoMonth(page: Page, year: number, month: number): Promise<void> {
|
||||
await page.goto(`/#/history?period=month&year=${year}&month=${month}`);
|
||||
await expect(page.getByRole("heading", { name: /history/i }).first()).toBeVisible();
|
||||
}
|
||||
|
||||
function chart(page: Page, group: string): Locator {
|
||||
return page.getByTestId(`group-chart-${group}`);
|
||||
}
|
||||
|
||||
function section(page: Page, group: string): Locator {
|
||||
return page.getByTestId(`history-section-${group}`);
|
||||
}
|
||||
|
||||
// Y-axis labels rendered by echarts: position=right uses text-anchor=start.
|
||||
// DOM order: axis name first, then ticks bottom→top (min, ..., max).
|
||||
async function yAxis(c: Locator): Promise<string[]> {
|
||||
const els = c.locator('svg text[text-anchor="start"]');
|
||||
await els.first().waitFor();
|
||||
return els.allTextContents();
|
||||
}
|
||||
|
||||
test.describe("axis and units", () => {
|
||||
test("grid ±2 kW bidirectional, 1 decimal", async ({ page }) => {
|
||||
await gotoDay(page, 2026, 3, 24);
|
||||
expect(await yAxis(chart(page, "grid"))).toEqual(["kW", "-2.0", "-1.0", "0.0", "1.0", "2.0"]);
|
||||
});
|
||||
|
||||
test("sub-1 kW switches to W, floored at 1000 W", async ({ page }) => {
|
||||
await gotoDay(page, 2026, 4, 1);
|
||||
expect(await yAxis(chart(page, "grid"))).toEqual(["W", "-1,000", "-500", "0", "500", "1,000"]);
|
||||
});
|
||||
|
||||
test("import-only stays symmetric", async ({ page }) => {
|
||||
await gotoDay(page, 2026, 4, 2);
|
||||
expect(await yAxis(chart(page, "grid"))).toEqual(["kW", "-2.0", "-1.0", "0.0", "1.0", "2.0"]);
|
||||
});
|
||||
|
||||
test("battery ±3 kW, 1 decimal", async ({ page }) => {
|
||||
await gotoDay(page, 2026, 4, 3);
|
||||
expect(await yAxis(chart(page, "battery"))).toEqual([
|
||||
"kW",
|
||||
"-3.0",
|
||||
"-1.5",
|
||||
"0.0",
|
||||
"1.5",
|
||||
"3.0",
|
||||
]);
|
||||
});
|
||||
|
||||
test("battery ±6 kW, integer labels", async ({ page }) => {
|
||||
await gotoDay(page, 2026, 4, 4);
|
||||
expect(await yAxis(chart(page, "battery"))).toEqual(["kW", "-6", "-3", "0", "3", "6"]);
|
||||
});
|
||||
|
||||
test("unidirectional axis stays positive", async ({ page }) => {
|
||||
await gotoDay(page, 2026, 4, 5);
|
||||
// Unidirectional groups use echarts auto-scale (min: 0). Peak 1.6 kW → ticks 0..2.
|
||||
expect(await yAxis(chart(page, "pv"))).toEqual(["kW", "0.0", "0.5", "1.0", "1.5", "2.0"]);
|
||||
});
|
||||
|
||||
test("all-zero data: W axis at 1000 W", async ({ page }) => {
|
||||
await gotoDay(page, 2026, 4, 6);
|
||||
await expect(section(page, "pv")).toBeVisible();
|
||||
expect(await yAxis(chart(page, "pv"))).toEqual(["W", "0", "250", "500", "750", "1,000"]);
|
||||
});
|
||||
|
||||
test("stacked entities + overlay not clipped", async ({ page }) => {
|
||||
await gotoDay(page, 2026, 5, 2);
|
||||
// Stacked east+west peak = 1.8 kWh × 4 = 7.2 kW → niceCeil = 8.
|
||||
expect(await yAxis(chart(page, "pv"))).toEqual(["kW", "0", "2", "4", "6", "8"]);
|
||||
});
|
||||
|
||||
test("month view uses kWh unit", async ({ page }) => {
|
||||
await gotoMonth(page, 2026, 6);
|
||||
expect(await yAxis(chart(page, "battery"))).toEqual([
|
||||
"kWh",
|
||||
"-2.0",
|
||||
"-1.0",
|
||||
"0.0",
|
||||
"1.0",
|
||||
"2.0",
|
||||
]);
|
||||
});
|
||||
});
|
||||
|
||||
test.describe("consumption breakdown", () => {
|
||||
// 2026-04-07: home = 1.0 kWh, Kitchen = 0.4 kWh, Office = 0.3 kWh,
|
||||
// virtual Others = home − meters = 0.3 kWh.
|
||||
test("home total, meter legend, virtual Others", async ({ page }) => {
|
||||
await gotoDay(page, 2026, 4, 7);
|
||||
const consumption = section(page, "meter");
|
||||
await expect(consumption).toBeVisible();
|
||||
|
||||
// Section header total comes from `home`, not the sum of meters.
|
||||
await expect(consumption.getByRole("heading")).toContainText("1.0 kWh");
|
||||
|
||||
// Entity legend lists Others (virtual) + explicit meters.
|
||||
const legend = consumption.getByRole("button");
|
||||
await expect(legend).toHaveCount(3);
|
||||
await expect(consumption).toContainText("Others");
|
||||
await expect(consumption).toContainText("Kitchen");
|
||||
await expect(consumption).toContainText("Office");
|
||||
|
||||
// Per-entity totals via fmtWh + POWER_UNIT.AUTO.
|
||||
await expect(consumption).toContainText("400 Wh"); // Kitchen
|
||||
await expect(consumption).toContainText("300 Wh"); // Office + Others both 0.3 kWh
|
||||
});
|
||||
|
||||
test("entity focus rescales axis and resets on unfocus", async ({ page }) => {
|
||||
await gotoDay(page, 2026, 4, 7);
|
||||
const consumption = section(page, "meter");
|
||||
const meterChart = chart(page, "meter");
|
||||
|
||||
// Stacked total per slot 1.0 kW → echarts auto-axis 0..1.2 in 1-decimal.
|
||||
expect(await yAxis(meterChart)).toEqual(["kW", "0.0", "0.3", "0.6", "0.9", "1.2"]);
|
||||
|
||||
// Focus Kitchen → peak 400 W → unit switches to W, floored at 1000 W.
|
||||
const kitchen = consumption.getByRole("button", { name: "Kitchen 400 Wh" });
|
||||
await kitchen.click();
|
||||
await expect.poll(() => yAxis(meterChart)).toEqual(["W", "0", "250", "500", "750", "1,000"]);
|
||||
|
||||
// Unfocus → axis returns to kW range, not stuck on 1000 W cap.
|
||||
await kitchen.click();
|
||||
await expect.poll(() => yAxis(meterChart)).toEqual(["kW", "0.0", "0.3", "0.6", "0.9", "1.2"]);
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -1,6 +1,3 @@
|
|||
DROP TABLE IF EXISTS `meters`;
|
||||
DROP TABLE IF EXISTS `entities`;
|
||||
|
||||
CREATE TABLE `entities` (
|
||||
`id` integer,
|
||||
`group` text,
|
||||
|
|
@ -18,14 +15,22 @@ CREATE TABLE `meters` (
|
|||
CREATE UNIQUE INDEX `meters_meter_ts` ON `meters`(`meter`, `ts`);
|
||||
|
||||
-- entities
|
||||
INSERT INTO `entities` VALUES (1, 'virtual', 'home');
|
||||
INSERT INTO `entities` VALUES (1, 'home', 'home');
|
||||
INSERT INTO `entities` VALUES (2, 'grid', 'grid');
|
||||
INSERT INTO `entities` VALUES (3, 'battery', 'battery');
|
||||
INSERT INTO `entities` VALUES (4, 'pv', 'solar');
|
||||
INSERT INTO `entities` VALUES (5, 'meter', 'Kitchen');
|
||||
INSERT INTO `entities` VALUES (6, 'meter', 'Office');
|
||||
INSERT INTO `entities` VALUES (7, 'pv', 'pv-east');
|
||||
INSERT INTO `entities` VALUES (8, 'pv', 'pv-west');
|
||||
INSERT INTO `entities` VALUES (9, 'forecast', 'forecast');
|
||||
|
||||
-- meter data: 6 slots per entity spanning midnight (local time +01:00)
|
||||
-- 2026-03-24: 22:00, 22:15, 22:30, 22:45 (4 slots)
|
||||
-- 2026-03-25: 00:00, 00:15 (2 slots)
|
||||
|
||||
-- home (id=1): import=0.1, export=0 per slot
|
||||
-- =====================================================================
|
||||
-- API test data: 2026-03-24/25 (existing). Used by the JSON-shape tests.
|
||||
-- 2026-03-24 22:00, 22:15, 22:30, 22:45 CET (+01:00) → 4 slots
|
||||
-- 2026-03-25 00:00, 00:15 CET → 2 slots
|
||||
-- =====================================================================
|
||||
-- home: import=0.1, export=0 per slot
|
||||
INSERT INTO `meters` VALUES (1, 1774386000, 0.1, 0);
|
||||
INSERT INTO `meters` VALUES (1, 1774386900, 0.1, 0);
|
||||
INSERT INTO `meters` VALUES (1, 1774387800, 0.1, 0);
|
||||
|
|
@ -33,10 +38,107 @@ INSERT INTO `meters` VALUES (1, 1774388700, 0.1, 0);
|
|||
INSERT INTO `meters` VALUES (1, 1774393200, 0.1, 0);
|
||||
INSERT INTO `meters` VALUES (1, 1774394100, 0.1, 0);
|
||||
|
||||
-- grid (id=2): import=0.5, export=0.1 per slot
|
||||
-- grid: import=0.5, export=0.1 per slot (peak 2 kW bidirectional)
|
||||
INSERT INTO `meters` VALUES (2, 1774386000, 0.5, 0.1);
|
||||
INSERT INTO `meters` VALUES (2, 1774386900, 0.5, 0.1);
|
||||
INSERT INTO `meters` VALUES (2, 1774387800, 0.5, 0.1);
|
||||
INSERT INTO `meters` VALUES (2, 1774388700, 0.5, 0.1);
|
||||
INSERT INTO `meters` VALUES (2, 1774393200, 0.5, 0.1);
|
||||
INSERT INTO `meters` VALUES (2, 1774394100, 0.5, 0.1);
|
||||
|
||||
-- =====================================================================
|
||||
-- UI / chart test data: one local Berlin day per case, 12:00-12:45 CEST.
|
||||
-- All slots are 15 minutes; energy column is kWh in DB.
|
||||
-- =====================================================================
|
||||
|
||||
-- 2026-04-01 → grid sub-1-kW range. peak 4 W → unit W, symmetric axis.
|
||||
-- 0.001 kWh × 4 = 0.004 kW = 4 W. Stored values must survive Wh-rounding
|
||||
-- in the backend (db_history.roundEnergy), so we use kWh values ≥ 0.001.
|
||||
INSERT INTO `meters` VALUES (2, 1775037600, 0.001, 0.001);
|
||||
INSERT INTO `meters` VALUES (2, 1775038500, 0.001, 0.001);
|
||||
INSERT INTO `meters` VALUES (2, 1775039400, 0.001, 0.001);
|
||||
INSERT INTO `meters` VALUES (2, 1775040300, 0.001, 0.001);
|
||||
|
||||
-- 2026-04-02 → grid import-only. peak 2 kW. Axis must still be symmetric
|
||||
-- because grid is in the hardcoded bidirectional list.
|
||||
INSERT INTO `meters` VALUES (2, 1775124000, 0.5, 0);
|
||||
INSERT INTO `meters` VALUES (2, 1775124900, 0.5, 0);
|
||||
INSERT INTO `meters` VALUES (2, 1775125800, 0.5, 0);
|
||||
INSERT INTO `meters` VALUES (2, 1775126700, 0.5, 0);
|
||||
|
||||
-- 2026-04-03 → battery 2.4 kW peak. niceCeil(2.4) = 3.
|
||||
-- Alternating slots: charge / discharge / charge / discharge.
|
||||
INSERT INTO `meters` VALUES (3, 1775210400, 0.6, 0);
|
||||
INSERT INTO `meters` VALUES (3, 1775211300, 0, 0.5);
|
||||
INSERT INTO `meters` VALUES (3, 1775212200, 0.6, 0);
|
||||
INSERT INTO `meters` VALUES (3, 1775213100, 0, 0.5);
|
||||
|
||||
-- 2026-04-04 → battery 4.8 kW peak. niceCeil(4.8) = 6 → integer kW labels.
|
||||
INSERT INTO `meters` VALUES (3, 1775296800, 1.2, 0);
|
||||
INSERT INTO `meters` VALUES (3, 1775297700, 0, 1.0);
|
||||
INSERT INTO `meters` VALUES (3, 1775298600, 1.2, 0);
|
||||
INSERT INTO `meters` VALUES (3, 1775299500, 0, 1.0);
|
||||
|
||||
-- 2026-04-05 → PV unidirectional. peak 1.6 kW.
|
||||
INSERT INTO `meters` VALUES (4, 1775383200, 0.4, 0);
|
||||
INSERT INTO `meters` VALUES (4, 1775384100, 0.4, 0);
|
||||
INSERT INTO `meters` VALUES (4, 1775385000, 0.4, 0);
|
||||
INSERT INTO `meters` VALUES (4, 1775385900, 0.4, 0);
|
||||
|
||||
-- 2026-04-06 → PV all-zero day (cloudy). Rows exist but values are 0.
|
||||
-- Regression: chart group must still render.
|
||||
INSERT INTO `meters` VALUES (4, 1775469600, 0, 0);
|
||||
INSERT INTO `meters` VALUES (4, 1775470500, 0, 0);
|
||||
INSERT INTO `meters` VALUES (4, 1775471400, 0, 0);
|
||||
INSERT INTO `meters` VALUES (4, 1775472300, 0, 0);
|
||||
|
||||
-- 2026-04-07 → consumption breakdown. home = 1.0 kWh total; explicit
|
||||
-- meters Kitchen 0.4 / Office 0.3; virtual "Others" = home − meters = 0.3.
|
||||
INSERT INTO `meters` VALUES (1, 1775556000, 0.25, 0);
|
||||
INSERT INTO `meters` VALUES (1, 1775556900, 0.25, 0);
|
||||
INSERT INTO `meters` VALUES (1, 1775557800, 0.25, 0);
|
||||
INSERT INTO `meters` VALUES (1, 1775558700, 0.25, 0);
|
||||
INSERT INTO `meters` VALUES (5, 1775556000, 0.1, 0);
|
||||
INSERT INTO `meters` VALUES (5, 1775556900, 0.1, 0);
|
||||
INSERT INTO `meters` VALUES (5, 1775557800, 0.1, 0);
|
||||
INSERT INTO `meters` VALUES (5, 1775558700, 0.1, 0);
|
||||
INSERT INTO `meters` VALUES (6, 1775556000, 0.075, 0);
|
||||
INSERT INTO `meters` VALUES (6, 1775556900, 0.075, 0);
|
||||
INSERT INTO `meters` VALUES (6, 1775557800, 0.075, 0);
|
||||
INSERT INTO `meters` VALUES (6, 1775558700, 0.075, 0);
|
||||
|
||||
-- 2026-05-02 → multi-entity PV with stacked peak 7.2 kW. Two PV entities
|
||||
-- (east + west) plus a forecast overlay. axisPeak comes from the stacked
|
||||
-- per-slot sum, niceCeil → 8 → integer kW labels, echarts auto-range.
|
||||
INSERT INTO `meters` VALUES (7, 1777687200, 0.05, 0); -- 04:00
|
||||
INSERT INTO `meters` VALUES (7, 1777694400, 0.30, 0); -- 06:00
|
||||
INSERT INTO `meters` VALUES (7, 1777701600, 0.60, 0); -- 08:00
|
||||
INSERT INTO `meters` VALUES (7, 1777708800, 0.80, 0); -- 10:00
|
||||
INSERT INTO `meters` VALUES (7, 1777716000, 1.00, 0); -- 12:00
|
||||
INSERT INTO `meters` VALUES (7, 1777723200, 0.70, 0); -- 14:00
|
||||
INSERT INTO `meters` VALUES (7, 1777730400, 0.30, 0); -- 16:00
|
||||
INSERT INTO `meters` VALUES (7, 1777737600, 0.05, 0); -- 18:00
|
||||
INSERT INTO `meters` VALUES (8, 1777687200, 0, 0);
|
||||
INSERT INTO `meters` VALUES (8, 1777694400, 0.10, 0);
|
||||
INSERT INTO `meters` VALUES (8, 1777701600, 0.40, 0);
|
||||
INSERT INTO `meters` VALUES (8, 1777708800, 0.60, 0);
|
||||
INSERT INTO `meters` VALUES (8, 1777716000, 0.80, 0);
|
||||
INSERT INTO `meters` VALUES (8, 1777723200, 1.00, 0);
|
||||
INSERT INTO `meters` VALUES (8, 1777730400, 0.80, 0);
|
||||
INSERT INTO `meters` VALUES (8, 1777737600, 0.30, 0);
|
||||
INSERT INTO `meters` VALUES (9, 1777687200, 0.05, 0);
|
||||
INSERT INTO `meters` VALUES (9, 1777694400, 0.40, 0);
|
||||
INSERT INTO `meters` VALUES (9, 1777701600, 1.00, 0);
|
||||
INSERT INTO `meters` VALUES (9, 1777708800, 1.40, 0);
|
||||
INSERT INTO `meters` VALUES (9, 1777716000, 1.80, 0);
|
||||
INSERT INTO `meters` VALUES (9, 1777723200, 1.70, 0);
|
||||
INSERT INTO `meters` VALUES (9, 1777730400, 1.10, 0);
|
||||
INSERT INTO `meters` VALUES (9, 1777737600, 0.35, 0);
|
||||
|
||||
-- =====================================================================
|
||||
-- Month-aggregation case: 2026-06, three days with battery bidirectional
|
||||
-- data. Daily totals ~1.4 / 1.0 kWh → limit rounds to 2 → 1-decimal kWh.
|
||||
-- =====================================================================
|
||||
INSERT INTO `meters` VALUES (3, 1781517600, 1.4, 1.0);
|
||||
INSERT INTO `meters` VALUES (3, 1781604000, 1.4, 1.0);
|
||||
INSERT INTO `meters` VALUES (3, 1781690400, 1.4, 1.0);
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue