diff --git a/packages/modules/web_themes/koala/source/src/components/ChartCarousel.vue b/packages/modules/web_themes/koala/source/src/components/ChartCarousel.vue
index 4d499f7259..d19916fdae 100644
--- a/packages/modules/web_themes/koala/source/src/components/ChartCarousel.vue
+++ b/packages/modules/web_themes/koala/source/src/components/ChartCarousel.vue
@@ -23,7 +23,10 @@
{
localDataStore.toggleLegendVisibility();
};
const legendVisible = computed(() => localDataStore.legendVisible);
+const { compact: sankeyCompact } = useSankeyData();
const fullscreen = ref(false);
const carouselKey = computed(() =>
diff --git a/packages/modules/web_themes/koala/source/src/components/charts/sankeyChart/SankeyChart.vue b/packages/modules/web_themes/koala/source/src/components/charts/sankeyChart/SankeyChart.vue
index 0eb67a4bc6..e1281f6c8d 100644
--- a/packages/modules/web_themes/koala/source/src/components/charts/sankeyChart/SankeyChart.vue
+++ b/packages/modules/web_themes/koala/source/src/components/charts/sankeyChart/SankeyChart.vue
@@ -12,6 +12,19 @@
Aktuell kein Energiefluss
+
+
+
+ {{ item.label }}
+
+
@@ -26,6 +39,7 @@ import type { SankeyDataPoint } from 'chartjs-chart-sankey';
import { useSankeyData } from './useSankeyData';
import { useSankeyHover } from './useSankeyHover';
import { createFlowLabels } from './sankey-flow-labels';
+import { NODE_HOUSE, NODE_HOUSE_REST } from './energy-allocation';
// A typed sankey component (like vue-chartjs's built-in Line/Bar) so the
// `data`/`options` props are fixed to 'sankey' instead of the whole ChartType
@@ -43,11 +57,12 @@ const ChartjsSankey = createTypedChart('sankey', [
defineOptions({ name: 'SankeyChart' });
-// Accepted for parity with the other carousel charts; not used here.
+// The legend is only shown in compact mode, where node names are hidden.
+// When chart has three columns.
defineProps<{ showLegend?: boolean }>();
const $q = useQuasar();
-const { allocation, colorForNode, labelColor } = useSankeyData();
+const { allocation, compact, colorForNode, labelColor } = useSankeyData();
const hasFlows = computed(() => allocation.value.edges.length > 0);
@@ -78,38 +93,55 @@ const flowLabels = createFlowLabels({
});
const chartPlugins = [hoverPlugin, flowLabels.plugin];
+// Assign a per-column priority from node power.
+// Sources descending (largest on top).
+// Sinks ascending (largest on the bottom). When the household splits into a
+// third column it goes on top instead, level with its parts, so the split
+// does not have to cross the other sinks.
+// House parts descending, the remainder last.
+const nodePriority = computed(() => {
+ const { sources, sinks, houseParts } = allocation.value;
+ const houseSplit = houseParts.length > 0;
+ const priority: Record = {};
+ [...sources]
+ .sort((a, b) => b.power - a.power)
+ .forEach((node, index) => {
+ priority[node.id] = index;
+ });
+ [...sinks]
+ .sort((a, b) => a.power - b.power)
+ .forEach((node, index) => {
+ priority[node.id] = houseSplit && node.id === NODE_HOUSE ? -1 : index;
+ });
+ [...houseParts]
+ .sort((a, b) =>
+ a.id === NODE_HOUSE_REST
+ ? 1
+ : b.id === NODE_HOUSE_REST
+ ? -1
+ : b.power - a.power,
+ )
+ .forEach((node, index) => {
+ priority[node.id] = index;
+ });
+ return priority;
+});
+
const chartData = computed>(() => {
// Reference the theme so a light/dark toggle rebuilds data and re-resolves
// the CSS color variables.
void $q.dark.isActive;
-
// Reading the hover state here (rather than in the color callbacks, which
// Chart.js calls later) is what makes this recompute on every hover change.
const flowColor = focusColors();
-
- const { edges, nodes, sources, sinks } = allocation.value;
+ const { edges, nodes } = allocation.value;
const labels: Record = {};
const columns: Record = {};
for (const node of nodes) {
labels[node.id] = node.label;
- columns[node.id] = sources.some((source) => source.id === node.id) ? 0 : 1;
+ columns[node.id] = node.column;
}
- // Assign a per-column priority from node power.
- // Sources descending (largest on top).
- // Sinks ascending (largest on the bottom).
- const priority: Record = {};
- [...sources]
- .sort((a, b) => b.power - a.power)
- .forEach((node, index) => {
- priority[node.id] = index;
- });
- [...sinks]
- .sort((a, b) => a.power - b.power)
- .forEach((node, index) => {
- priority[node.id] = index;
- });
-
const textColor = labelColor();
return {
@@ -123,7 +155,7 @@ const chartData = computed>(() => {
})),
labels,
column: columns,
- priority,
+ priority: nodePriority.value,
colorFrom: (ctx) =>
flowColor(
(ctx.raw as SankeyDataPoint | undefined)?.from ?? '',
@@ -137,7 +169,7 @@ const chartData = computed>(() => {
colorMode: 'gradient',
borderWidth: 0,
color: textColor,
- nodeLabels: { color: textColor },
+ nodeLabels: { color: textColor, display: !compact.value },
nodeWidth: 16,
nodePadding: 12,
},
@@ -145,6 +177,21 @@ const chartData = computed>(() => {
};
});
+const legendItems = computed(() => {
+ void $q.dark.isActive;
+ const priority = nodePriority.value;
+ return [...allocation.value.nodes]
+ .sort(
+ (a, b) =>
+ a.column - b.column || (priority[a.id] ?? 0) - (priority[b.id] ?? 0),
+ )
+ .map((node) => ({
+ id: node.id,
+ label: node.label,
+ color: colorForNode(node.id),
+ }));
+});
+
const chartOptions = computed>(() => ({
responsive: true,
maintainAspectRatio: false,
@@ -184,6 +231,19 @@ const chartOptions = computed>(() => ({
height: 100% !important;
}
+.legend-wrapper {
+ flex: 0 0 auto;
+ gap: 2px 10px;
+}
+
+.legend-swatch {
+ width: 10px;
+ height: 10px;
+ border-radius: 2px;
+ margin-right: 4px;
+ flex: 0 0 auto;
+}
+
.sankey-empty {
height: 100%;
display: flex;
diff --git a/packages/modules/web_themes/koala/source/src/components/charts/sankeyChart/energy-allocation.ts b/packages/modules/web_themes/koala/source/src/components/charts/sankeyChart/energy-allocation.ts
index 6a256e6127..ee90200bcd 100644
--- a/packages/modules/web_themes/koala/source/src/components/charts/sankeyChart/energy-allocation.ts
+++ b/packages/modules/web_themes/koala/source/src/components/charts/sankeyChart/energy-allocation.ts
@@ -1,8 +1,14 @@
-/** Fixed node ids. Chargepoints/consumers use dynamic ids (see input). */
+/** Fixed node ids. Chargepoints/consumers/counters use dynamic ids (see input). */
export const NODE_GRID = 'grid';
export const NODE_PV = 'pv';
export const NODE_BATTERY = 'battery';
export const NODE_HOUSE = 'house';
+export const NODE_HOUSE_REST = 'house_rest';
+
+/** Column of each node in the diagram, left to right. */
+export const COLUMN_SOURCE = 0;
+export const COLUMN_SINK = 1;
+export const COLUMN_HOUSE_PART = 2;
/**
* Raw signed power per component, using the same sign conventions as the
@@ -10,7 +16,12 @@ export const NODE_HOUSE = 'house';
* grid : + import (source) | - export (sink)
* pv : - production (source)| + consumption (rare)
* battery : + charging (sink) | - discharging (source)
- * cp/consumer power: + drawing (sink) | - feeding back, e.g. V2G (source)
+ * cp/consumer/counter power: + drawing (sink) | - feeding back, e.g. V2G (source)
+ *
+ * Consumers and counters flagged `inHouse` are counted in the home
+ * consumption. They are not drawn as sinks of their own but as parts of the
+ * household node, which splits into them and the remaining household
+ * consumption in a third column.
*/
export interface EnergyFlowInput {
grid: number;
@@ -18,6 +29,10 @@ export interface EnergyFlowInput {
battery: number;
chargePoints: DynamicNodeInput[];
consumers: DynamicNodeInput[];
+ //Secondary counters with their own share of power: their power minus the
+ //components below them, which are nodes of their own.
+
+ counters?: DynamicNodeInput[];
/**
* Hybrid inverter/battery pairs. A hybrid battery charges from its inverter's
* PV directly on the shared DC bus, before the remaining PV mixes with grid
@@ -35,12 +50,14 @@ export interface DynamicNodeInput {
id: string;
label: string;
power: number;
+ inHouse?: boolean;
}
export interface FlowNode {
id: string;
label: string;
power: number;
+ column: number;
}
export interface FlowEdge {
@@ -55,6 +72,8 @@ export interface AllocationResult {
nodes: FlowNode[];
sources: FlowNode[];
sinks: FlowNode[];
+ /** Consumers and counters counted in the home consumption plus the remainder. */
+ houseParts: FlowNode[];
totalSources: number;
totalSinks: number;
imbalance: number;
@@ -70,55 +89,93 @@ const MIN_EDGE_WATTS = 1;
function classifyNodes(input: EnergyFlowInput): {
sources: FlowNode[];
sinks: FlowNode[];
+ houseParts: FlowNode[];
} {
const sources: FlowNode[] = [];
const sinks: FlowNode[] = [];
+ const houseParts: FlowNode[] = [];
+ const source = (id: string, label: string, power: number) =>
+ sources.push({ id, label, power, column: COLUMN_SOURCE });
+ const sink = (id: string, label: string, power: number) =>
+ sinks.push({ id, label, power, column: COLUMN_SINK });
if (input.grid > 0) {
- sources.push({ id: NODE_GRID, label: 'Netz', power: input.grid });
+ source(NODE_GRID, 'Netz', input.grid);
} else if (input.grid < 0) {
- sinks.push({ id: NODE_GRID, label: 'Netz', power: -input.grid });
+ sink(NODE_GRID, 'Netz', -input.grid);
}
if (input.pv < 0) {
- sources.push({ id: NODE_PV, label: 'PV', power: -input.pv });
+ source(NODE_PV, 'PV', -input.pv);
}
if (input.battery > 0) {
- sinks.push({ id: NODE_BATTERY, label: 'Speicher', power: input.battery });
+ sink(NODE_BATTERY, 'Speicher', input.battery);
} else if (input.battery < 0) {
- sources.push({
- id: NODE_BATTERY,
- label: 'Speicher',
- power: -input.battery,
- });
+ source(NODE_BATTERY, 'Speicher', -input.battery);
}
for (const cp of input.chargePoints) {
if (cp.power > 0) {
- sinks.push({ id: cp.id, label: cp.label, power: cp.power });
+ sink(cp.id, cp.label, cp.power);
} else if (cp.power < 0) {
- sources.push({ id: cp.id, label: cp.label, power: -cp.power });
+ source(cp.id, cp.label, -cp.power);
}
}
- for (const consumer of input.consumers) {
- if (consumer.power > 0) {
- sinks.push({ id: consumer.id, label: consumer.label, power: consumer.power });
- } else if (consumer.power < 0) {
- sources.push({ id: consumer.id, label: consumer.label, power: -consumer.power });
+ for (const load of [...input.consumers, ...(input.counters ?? [])]) {
+ if (load.power > 0 && load.inHouse) {
+ houseParts.push({
+ id: load.id,
+ label: load.label,
+ power: load.power,
+ column: COLUMN_HOUSE_PART,
+ });
+ } else if (load.power > 0) {
+ sink(load.id, load.label, load.power);
+ } else if (load.power < 0) {
+ source(load.id, load.label, -load.power);
}
}
- // Household consumption = balancing residual.
+ // Household consumption = balancing residual. It includes the consumers and
+ // counters counted in the home consumption, as they are not sinks of their
+ // own.
const totalSources = sumPower(sources);
const otherSinks = sumPower(sinks);
const house = Math.max(0, totalSources - otherSinks);
if (house > 0) {
- sinks.push({ id: NODE_HOUSE, label: 'Hausverbrauch', power: house });
+ sink(NODE_HOUSE, 'Hausverbrauch', house);
}
- return { sources, sinks };
+ return { sources, sinks, houseParts: splitHouse(house, houseParts) };
+}
+
+/**
+ * Split the household node into the consumers and counters counted in it and
+ * the rest. Measurement noise can make those parts add up to more than the
+ * residual; they are then scaled down so the household node still balances.
+ */
+function splitHouse(house: number, houseParts: FlowNode[]): FlowNode[] {
+ if (house <= 0 || houseParts.length === 0) {
+ return [];
+ }
+ const partsTotal = sumPower(houseParts);
+ const scale = Math.min(1, house / partsTotal);
+ const parts = houseParts.map((node) => ({
+ ...node,
+ power: node.power * scale,
+ }));
+ const rest = house - partsTotal * scale;
+ if (rest >= MIN_EDGE_WATTS) {
+ parts.push({
+ id: NODE_HOUSE_REST,
+ label: 'Sonstiger Hausverbrauch',
+ power: rest,
+ column: COLUMN_HOUSE_PART,
+ });
+ }
+ return parts;
}
function sumPower(nodes: FlowNode[]): number {
@@ -132,8 +189,8 @@ export interface GroupOptions {
}
/**
- * Collapse a list of dynamic nodes (chargePoints or consumers) into a single
- * aggregated node once it exceeds the threshold.
+ * Collapse a list of dynamic nodes (chargePoints, consumers or counters) into
+ * a single aggregated node once it exceeds the threshold.
*/
export function groupNodes(
nodes: DynamicNodeInput[],
@@ -179,7 +236,7 @@ function mergeEdges(edges: FlowEdge[]): FlowEdge[] {
* With no hybrid pairs this reduces to plain uniform proportional.
*/
export function allocate(input: EnergyFlowInput): AllocationResult {
- const { sources, sinks } = classifyNodes(input);
+ const { sources, sinks, houseParts } = classifyNodes(input);
const totalSources = sumPower(sources);
const totalSinks = sumPower(sinks);
@@ -235,11 +292,19 @@ export function allocate(input: EnergyFlowInput): AllocationResult {
}
}
+ // Second stage: household -> the consumers counted in it and the rest.
+ for (const part of houseParts) {
+ if (part.power >= MIN_EDGE_WATTS) {
+ edges.push({ from: NODE_HOUSE, to: part.id, flow: part.power });
+ }
+ }
+
return {
edges: mergeEdges(edges),
- nodes: [...sources, ...sinks],
+ nodes: [...sources, ...sinks, ...houseParts],
sources,
sinks,
+ houseParts,
totalSources,
totalSinks,
imbalance: totalSources - totalSinks,
diff --git a/packages/modules/web_themes/koala/source/src/components/charts/sankeyChart/useSankeyData.ts b/packages/modules/web_themes/koala/source/src/components/charts/sankeyChart/useSankeyData.ts
index 4d554bc830..e8bd9b9ae9 100644
--- a/packages/modules/web_themes/koala/source/src/components/charts/sankeyChart/useSankeyData.ts
+++ b/packages/modules/web_themes/koala/source/src/components/charts/sankeyChart/useSankeyData.ts
@@ -2,21 +2,28 @@
* Store adapter for the live Sankey diagram.
*/
import { computed } from 'vue';
+import { useQuasar } from 'quasar';
import { useMqttStore } from 'src/stores/mqtt-store';
import {
allocate,
groupNodes,
type AllocationResult,
+ type DynamicNodeInput,
} from './energy-allocation';
-// Aggregate chargepoints/consumers into a single node once there are more than the threshold.
+// Aggregate chargepoints/consumers/counters into a single node once there are more than the threshold.
const GROUP_THRESHOLD = 3;
const CP_GROUP_ID = 'cp_group';
const CONSUMER_GROUP_ID = 'consumer_group';
+const CONSUMER_HOUSE_GROUP_ID = 'consumer_house_group';
const CONSUMER_ID_PREFIX = 'consumer';
+const COUNTER_GROUP_ID = 'counter_group';
+const COUNTER_HOUSE_GROUP_ID = 'counter_house_group';
+const COUNTER_ID_PREFIX = 'counter';
export function useSankeyData() {
const mqttStore = useMqttStore();
+ const $q = useQuasar();
const num = (value: unknown): number => Number(value) || 0;
@@ -31,28 +38,71 @@ export function useSankeyData() {
),
);
- const consumers = computed(() =>
- groupNodes(
- mqttStore.consumerIds.map((id) => ({
- id: `${CONSUMER_ID_PREFIX}${id}`,
- label: mqttStore.consumerName(id) || `Verbraucher ${id}`,
- power: num(mqttStore.consumerPower(id, 'value')),
- })),
- {
- threshold: GROUP_THRESHOLD,
- id: CONSUMER_GROUP_ID,
- label: 'Verbraucher',
- },
- ),
- );
+ // Consumers and counters counted in the home consumption are drawn as parts
+ // of the household node, the others as sinks of their own. Group each set on
+ // its own so the two never collapse into one node.
+ const groupInAndOut = (
+ nodes: DynamicNodeInput[],
+ label: string,
+ houseGroupId: string,
+ groupId: string,
+ ) => {
+ const group = (inHouse: boolean, id: string) =>
+ groupNodes(
+ nodes.filter((node) => (node.inHouse ?? false) === inHouse),
+ { threshold: GROUP_THRESHOLD, id, label },
+ ).map((node) => ({ ...node, inHouse }));
+ return [...group(true, houseGroupId), ...group(false, groupId)];
+ };
+
+ const consumers = computed(() => {
+ const inHouse = mqttStore.inHomeConsumption.consumerIds;
+ const nodes = mqttStore.consumerIds.map((id) => ({
+ id: `${CONSUMER_ID_PREFIX}${id}`,
+ label: mqttStore.consumerName(id) || `Verbraucher ${id}`,
+ power: num(mqttStore.consumerPower(id, 'value')),
+ inHouse: inHouse.includes(id),
+ }));
+ return groupInAndOut(
+ nodes,
+ 'Verbraucher',
+ CONSUMER_HOUSE_GROUP_ID,
+ CONSUMER_GROUP_ID,
+ );
+ });
+
+ const counters = computed(() => {
+ const { inHome, notInHome } = mqttStore.shownCounterIds;
+ const node = (id: number, inHouse: boolean) => ({
+ id: `${COUNTER_ID_PREFIX}${id}`,
+ label: mqttStore.componentName(id) || `Zähler ${id}`,
+ power: num(mqttStore.counterLocalPower('value', id)),
+ inHouse,
+ });
+ return groupInAndOut(
+ [
+ ...inHome.map((id) => node(id, true)),
+ ...notInHome.map((id) => node(id, false)),
+ ],
+ 'Zähler',
+ COUNTER_HOUSE_GROUP_ID,
+ COUNTER_GROUP_ID,
+ );
+ });
// Hybrid inverter/battery pairs: how much of each hybrid battery's charge is
// covered by its own inverter's PV on the DC bus. pvPowerIndividual reports
// production as negative; batteryPower reports charging as positive.
const hybrid = computed(() =>
mqttStore.hybridInverters.map(({ inverterId, batteryId }) => ({
- inverterPv: Math.max(0, -num(mqttStore.pvPowerIndividual(inverterId, 'value'))),
- batteryCharge: Math.max(0, num(mqttStore.batteryPower(batteryId, 'value'))),
+ inverterPv: Math.max(
+ 0,
+ -num(mqttStore.pvPowerIndividual(inverterId, 'value')),
+ ),
+ batteryCharge: Math.max(
+ 0,
+ num(mqttStore.batteryPower(batteryId, 'value')),
+ ),
})),
);
@@ -65,10 +115,17 @@ export function useSankeyData() {
: 0,
chargePoints: chargePoints.value,
consumers: consumers.value,
+ counters: counters.value,
hybrid: hybrid.value,
}),
);
+ // A small screen has no room for node names once the household splits into
+ // a third column. The names are then hidden and listed in a legend instead.
+ const compact = computed(() => {
+ return $q.screen.lt.sm && allocation.value.houseParts.length > 0;
+ });
+
/**
* Resolve the display color for a node id.
*/
@@ -81,24 +138,37 @@ export function useSankeyData() {
case 'battery':
return cssVar('--q-battery-stroke');
case 'house':
+ case 'house_rest':
return cssVar('--q-home-stroke');
+ case COUNTER_GROUP_ID:
+ case COUNTER_HOUSE_GROUP_ID:
+ return cssVar('--q-secondary-counter-stroke');
case CP_GROUP_ID:
return cssVar('--q-charge-point-stroke');
case CONSUMER_GROUP_ID:
+ case CONSUMER_HOUSE_GROUP_ID:
return cssVar('--q-consumer');
}
if (id.startsWith('cp')) {
const cpId = Number(id.slice(2));
- return mqttStore.chargePointColor(cpId) || cssVar('--q-charge-point-stroke');
+ return (
+ mqttStore.chargePointColor(cpId) || cssVar('--q-charge-point-stroke')
+ );
}
if (id.startsWith(CONSUMER_ID_PREFIX)) {
const consumerId = Number(id.slice(CONSUMER_ID_PREFIX.length));
return mqttStore.consumerColor(consumerId) || cssVar('--q-consumer');
}
+ if (id.startsWith(COUNTER_ID_PREFIX)) {
+ const counterId = Number(id.slice(COUNTER_ID_PREFIX.length));
+ return (
+ mqttStore.secondaryCounterColor(counterId) ||
+ cssVar('--q-secondary-counter-stroke')
+ );
+ }
return cssVar('--q-vehicle-stroke');
};
-
//Node-label color for dark mode / light mode.
const labelColor = (): string => {
if (typeof document === 'undefined') {
@@ -107,7 +177,7 @@ export function useSankeyData() {
return getComputedStyle(document.body).color || '#000000';
};
- return { allocation, colorForNode, labelColor };
+ return { allocation, compact, colorForNode, labelColor };
}
/**
diff --git a/packages/modules/web_themes/koala/source/src/stores/mqtt-store.ts b/packages/modules/web_themes/koala/source/src/stores/mqtt-store.ts
index b245f09246..17ba8a3dbb 100644
--- a/packages/modules/web_themes/koala/source/src/stores/mqtt-store.ts
+++ b/packages/modules/web_themes/koala/source/src/stores/mqtt-store.ts
@@ -4267,6 +4267,32 @@ export const useMqttStore = defineStore('mqtt', () => {
};
});
+ /**
+ * Get the counter's own share of power: its power minus the power of the
+ * components directly below it. identified by counterId
+ * @param returnType type of return value, 'textValue', 'value', 'scaledValue', 'scaledUnit' or 'object'
+ * @param counterId counter ID
+ * @returns string | number | ValueObject | undefined
+ */
+ const counterLocalPower = computed(() => {
+ return (returnType: string = 'textValue', counterId?: number) => {
+ const id = counterId;
+ let localPower = undefined;
+ if (id !== undefined) {
+ localPower = getValue.value(`openWB/counter/${id}/get/local_power`) as
+ number | undefined;
+ }
+ const valueObject = getValueObject.value(localPower);
+ if (returnType in valueObject) {
+ return valueObject[returnType as keyof ValueObject];
+ }
+ if (returnType == 'object') {
+ return valueObject;
+ }
+ console.error('returnType not found!', returnType, localPower);
+ };
+ });
+
/**
* Get daily counter energy imported identified by root grid counter in component hierarchy or counterId
* @param returnType type of return value, 'textValue', 'value', 'scaledValue', 'scaledUnit' or 'object'
@@ -4424,6 +4450,7 @@ export const useMqttStore = defineStore('mqtt', () => {
const ids = () => ({
consumerIds: [] as number[],
counterIds: [] as number[],
+ counterIdsWithChildren: [] as number[],
});
const result = { inHome: ids(), notInHome: ids() };
function walk(
@@ -4442,10 +4469,19 @@ export const useMqttStore = defineStore('mqtt', () => {
AUTO,
) as string | undefined;
const mode = own && own !== AUTO ? own : parentMode;
+ let bucket;
if (mode === NOT_HOME) {
- result.notInHome.counterIds.push(node.id);
- } else if (!isRoot) {
- result.inHome.counterIds.push(node.id);
+ bucket = result.notInHome;
+ } else if (isRoot) {
+ bucket = undefined;
+ } else {
+ bucket = result.inHome;
+ }
+ if (bucket) {
+ bucket.counterIds.push(node.id);
+ if (node.children?.length) {
+ bucket.counterIdsWithChildren.push(node.id);
+ }
}
walk(node.children, mode, false);
} else if (node.type === 'consumer') {
@@ -4490,6 +4526,34 @@ export const useMqttStore = defineStore('mqtt', () => {
() => homeConsumptionAssignment.value.notInHome,
);
+ // own share in W below which a counter with components below it is hidden
+ // (threshold for showing in the home consumption details)
+ const COUNTER_OWN_SHARE_THRESHOLD = 50;
+
+ /**
+ * Get the counters to list in the home consumption details. A counter with
+ * components below it is left out if its own share is negligible, as it
+ * then only measures those components.
+ * @returns object with the keys inHome and notInHome, each holding counter ids
+ */
+ const shownCounterIds = computed(() => {
+ const shown = (bucket: {
+ counterIds: number[];
+ counterIdsWithChildren: number[];
+ }) => {
+ // a counter that only measures the components below it is hidden
+ const hidden = (id: number) =>
+ bucket.counterIdsWithChildren.includes(id) &&
+ Math.abs(Number(counterLocalPower.value('value', id) ?? 0)) <
+ COUNTER_OWN_SHARE_THRESHOLD;
+ return bucket.counterIds.filter((id) => !hidden(id));
+ };
+ return {
+ inHome: shown(inHomeConsumption.value),
+ notInHome: shown(notInHomeConsumption.value),
+ };
+ });
+
/**
* Get the summed power of all consumers and counters that are not counted
* in the home consumption.
@@ -4899,6 +4963,7 @@ export const useMqttStore = defineStore('mqtt', () => {
componentName,
gridComponentColor,
counterPower,
+ counterLocalPower,
counterDailyImported,
counterDailyExported,
// Home data
@@ -4906,6 +4971,7 @@ export const useMqttStore = defineStore('mqtt', () => {
homeDailyYield,
inHomeConsumption,
notInHomeConsumption,
+ shownCounterIds,
notInHomeConsumptionPower,
unmeasuredHomeConsumptionPower,
// PV data