diff --git a/packages/modules/web_themes/koala/source/src/assets/icons/owbInformation.svg b/packages/modules/web_themes/koala/source/src/assets/icons/owbInformation.svg
new file mode 100644
index 0000000000..c7f64746bf
--- /dev/null
+++ b/packages/modules/web_themes/koala/source/src/assets/icons/owbInformation.svg
@@ -0,0 +1 @@
+
diff --git a/packages/modules/web_themes/koala/source/src/assets/icons/owbMiscDevices.svg b/packages/modules/web_themes/koala/source/src/assets/icons/owbMiscDevices.svg
new file mode 100644
index 0000000000..863b457dcf
--- /dev/null
+++ b/packages/modules/web_themes/koala/source/src/assets/icons/owbMiscDevices.svg
@@ -0,0 +1,28 @@
+
diff --git a/packages/modules/web_themes/koala/source/src/components/charts/energyFlowChart/EnergyFlowChart.vue b/packages/modules/web_themes/koala/source/src/components/charts/energyFlowChart/EnergyFlowChart.vue
index fd54d0b0ed..2017b1dfc8 100644
--- a/packages/modules/web_themes/koala/source/src/components/charts/energyFlowChart/EnergyFlowChart.vue
+++ b/packages/modules/web_themes/koala/source/src/components/charts/energyFlowChart/EnergyFlowChart.vue
@@ -11,6 +11,10 @@ import HouseIcon from 'src/assets/icons/owbHouse.svg?component';
import VehicleIcon from 'src/assets/icons/owbVehicle.svg?component';
import ChargePointIcon from 'src/assets/icons/owbChargePoint_2.svg?component';
import ConsumerIcon from 'src/assets/icons/owbConsumer.svg?component';
+import CounterIcon from 'src/assets/icons/owbCounter.svg?component';
+import InfoIcon from 'src/assets/icons/owbInformation.svg?component';
+import MiscDevicesIcon from 'src/assets/icons/owbMiscDevices.svg?component';
+import HomeConsumptionDetailsDialog from './HomeConsumptionDetailsDialog.vue';
const mqttStore = useMqttStore();
const $q = useQuasar();
@@ -51,6 +55,12 @@ const svgViewBox = computed(
// the inset leaves a visible gap between circle and pill edge.
const iconCircleRadius = computed(() => svgSize.value.circleRadius - 2);
+const infoBadgeRadius = 3.2;
+const infoIconSize = 6;
+const infoBadgeOffset = computed(
+ () => (iconCircleRadius.value + 1) * Math.SQRT1_2,
+);
+
const svgIconWidth = computed(() => svgSize.value.circleRadius);
const svgIconHeight = computed(() => svgSize.value.circleRadius);
@@ -103,9 +113,79 @@ const homeConsumption = computed(() => Number(homePower.value.value) > 0);
const homeProduction = computed(() => Number(homePower.value.value) < 0);
const consumerPower = computed(
- () => mqttStore.consumerSumPower('object') as ValueObject,
+ () => mqttStore.notInHomeConsumptionPower('object') as ValueObject,
+);
+const consumerConsumption = computed(
+ () => Number(consumerPower.value.value) > 0,
+);
+const consumerProduction = computed(
+ () => Number(consumerPower.value.value) < 0,
+);
+const countersNotInHome = computed(
+ () => mqttStore.notInHomeConsumption.counterIds.length > 0,
+);
+const notInHomeVariants = {
+ consumer: {
+ label: 'Verbraucher',
+ class: 'consumer',
+ iconComponent: ConsumerIcon,
+ iconColor: 'var(--q-consumer)',
+ },
+ counter: {
+ label: 'Zähler',
+ class: 'not-in-home',
+ iconComponent: CounterIcon,
+ iconColor: 'var(--q-secondary-counter-stroke)',
+ },
+ mixed: {
+ label: 'Sonstiges',
+ class: 'not-in-home',
+ iconComponent: MiscDevicesIcon,
+ iconColor: 'var(--q-secondary-counter-stroke)',
+ },
+};
+const notInHomeVariant = computed(() => {
+ if (!countersNotInHome.value) {
+ return notInHomeVariants.consumer;
+ }
+ return mqttStore.notInHomeConsumption.consumerIds.length > 0
+ ? notInHomeVariants.mixed
+ : notInHomeVariants.counter;
+});
+const detailsVisible = ref(false);
+const detailsScope = ref<'inHome' | 'notInHome'>('inHome');
+const detailsTitle = computed(() =>
+ detailsScope.value === 'inHome'
+ ? 'Im Hausverbrauch enthalten:'
+ : notInHomeVariant.value.label,
+);
+
+const openDetails = (event: MouseEvent, component: FlowComponent) => {
+ if (!component.showInfo) {
+ return;
+ }
+ event.stopPropagation();
+ detailsScope.value = component.id === 'home' ? 'inHome' : 'notInHome';
+ detailsVisible.value = true;
+};
+const homeHasDetails = computed(
+ () =>
+ mqttStore.inHomeConsumption.consumerIds.length > 0 ||
+ mqttStore.inHomeConsumption.counterIds.length > 0,
+);
+const notInHomeHasDetails = computed(
+ () =>
+ mqttStore.notInHomeConsumption.consumerIds.length > 0 ||
+ mqttStore.notInHomeConsumption.counterIds.length > 0,
+);
+const showConsumerPower = computed(
+ () =>
+ mqttStore.notInHomeConsumption.consumerIds.length > 0 ||
+ countersNotInHome.value ||
+ // e.g. SmartHome devices excluded from the home consumption
+ consumerConsumption.value ||
+ consumerProduction.value,
);
-const showConsumerPower = computed(() => mqttStore.consumerIds.length > 0);
const pvPower = computed(() => mqttStore.pvPowerTotal('object') as ValueObject);
const pvProduction = computed(() => {
@@ -380,6 +460,7 @@ const svgComponents = computed((): FlowComponent[] => {
powerValue: Number(homePower.value.value),
iconComponent: HouseIcon,
iconColor: 'var(--q-home-stroke)',
+ showInfo: homeHasDetails.value,
});
}
@@ -387,18 +468,19 @@ const svgComponents = computed((): FlowComponent[] => {
components.push({
id: 'consumer',
class: {
- base: 'consumer',
- valueLabelColor: 'var(--q-consumer)',
- animatedReverse: Number(consumerPower.value.value) > 0,
+ base: notInHomeVariant.value.class,
+ animated: consumerProduction.value,
+ animatedReverse: consumerConsumption.value,
},
position: { row: 0, column: 1 },
label: [
- 'Verbraucher',
+ notInHomeVariant.value.label,
absoluteValueObject(consumerPower.value).textValue,
],
powerValue: Number(consumerPower.value.value),
- iconComponent: ConsumerIcon,
- iconColor: 'var(--q-consumer)',
+ iconComponent: notInHomeVariant.value.iconComponent,
+ iconColor: notInHomeVariant.value.iconColor,
+ showInfo: notInHomeHasDetails.value,
});
}
@@ -873,6 +955,8 @@ const labelClipPath = computed(() => {
{
:style="{ color: component.iconColor }"
/>
+
+
+
+
+
+
+
+
@@ -980,11 +1091,18 @@ path.animatedReverse.home {
animation-duration: v-bind('animationDurations.home');
}
+path.animated.consumer,
path.animatedReverse.consumer {
color: var(--q-consumer);
animation-duration: v-bind('animationDurations.consumer');
}
+path.animated.not-in-home,
+path.animatedReverse.not-in-home {
+ color: var(--q-secondary-counter-stroke);
+ animation-duration: v-bind('animationDurations.consumer');
+}
+
path.animated.pv,
path.animatedReverse.pv {
color: var(--q-pv-stroke);
@@ -1059,6 +1177,10 @@ circle:not(.soc) {
fill: var(--q-card-background);
}
+.info-badge .info-icon {
+ color: var(--q-primary);
+}
+
rect {
fill: var(--q-card-background);
}
diff --git a/packages/modules/web_themes/koala/source/src/components/charts/energyFlowChart/HomeConsumptionDetailsDialog.vue b/packages/modules/web_themes/koala/source/src/components/charts/energyFlowChart/HomeConsumptionDetailsDialog.vue
new file mode 100644
index 0000000000..cabef6a752
--- /dev/null
+++ b/packages/modules/web_themes/koala/source/src/components/charts/energyFlowChart/HomeConsumptionDetailsDialog.vue
@@ -0,0 +1,144 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
{{ item.name }}
+
{{ item.power }}
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/packages/modules/web_themes/koala/source/src/components/charts/energyFlowChart/energy-flow-chart-models.ts b/packages/modules/web_themes/koala/source/src/components/charts/energyFlowChart/energy-flow-chart-models.ts
index 9db5e13293..ddbce62a49 100644
--- a/packages/modules/web_themes/koala/source/src/components/charts/energyFlowChart/energy-flow-chart-models.ts
+++ b/packages/modules/web_themes/koala/source/src/components/charts/energyFlowChart/energy-flow-chart-models.ts
@@ -35,4 +35,5 @@ export interface FlowComponent {
soc?: number;
iconComponent?: Component;
iconColor?: string;
+ showInfo?: boolean;
}
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 443400c0d1..999dc0c400 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
@@ -4385,6 +4385,128 @@ export const useMqttStore = defineStore('mqtt', () => {
return result;
});
+ /**
+ * Sort the consumers and sub-counters by whether they are counted in the
+ * home consumption. The root counter is only listed if it is excluded.
+ * @returns object with the keys inHome and notInHome, each holding the ids
+ * of the consumers and counters
+ */
+ const homeConsumptionAssignment = computed(() => {
+ const AUTO = 'auto_home_consumption';
+ const NOT_HOME = 'no_home_consumption';
+ const ids = () => ({
+ consumerIds: [] as number[],
+ counterIds: [] as number[],
+ });
+ const result = { inHome: ids(), notInHome: ids() };
+ function walk(
+ nodes: Hierarchy[] | undefined,
+ parentMode: string,
+ isRoot: boolean,
+ ) {
+ if (nodes === undefined) {
+ return;
+ }
+ nodes.forEach((node) => {
+ if (node.type === 'counter') {
+ const own = getValue.value(
+ `openWB/counter/${node.id}/config/is_home_consumption_counter`,
+ undefined,
+ AUTO,
+ ) as string | undefined;
+ const mode = own && own !== AUTO ? own : parentMode;
+ if (mode === NOT_HOME) {
+ result.notInHome.counterIds.push(node.id);
+ } else if (!isRoot) {
+ result.inHome.counterIds.push(node.id);
+ }
+ walk(node.children, mode, false);
+ } else if (node.type === 'consumer') {
+ const own = getValue.value(
+ `openWB/consumer/${node.id}/config`,
+ 'is_home_consumption_consumer',
+ AUTO,
+ ) as string | undefined;
+ if ((own && own !== AUTO ? own : parentMode) === NOT_HOME) {
+ result.notInHome.consumerIds.push(node.id);
+ } else {
+ result.inHome.consumerIds.push(node.id);
+ }
+ } else {
+ walk(node.children, parentMode, false);
+ }
+ });
+ }
+ walk(
+ getValue.value('openWB/counter/get/hierarchy') as Hierarchy[],
+ 'home_consumption',
+ true,
+ );
+ return result;
+ });
+
+ /**
+ * Get the consumers and sub-counters that are counted in the home
+ * consumption.
+ * @returns object with the ids of the consumers and counters
+ */
+ const inHomeConsumption = computed(
+ () => homeConsumptionAssignment.value.inHome,
+ );
+
+ /**
+ * Get the consumers and counters that are not counted in the home
+ * consumption.
+ * @returns object with the ids of the consumers and counters
+ */
+ const notInHomeConsumption = computed(
+ () => homeConsumptionAssignment.value.notInHome,
+ );
+
+ /**
+ * Get the summed power of all consumers and counters that are not counted
+ * in the home consumption.
+ * @param returnType type of return value, 'textValue', 'value', 'scaledValue', 'scaledUnit' or 'object'
+ * @returns string | number | ValueObject
+ */
+ const notInHomeConsumptionPower = computed(() => {
+ return (returnType: string = 'textValue') => {
+ const power = getValue.value(
+ 'openWB/counter/set/not_in_home_consumption',
+ ) as number | undefined;
+ const valueObject = getValueObject.value(power);
+ if (Object.hasOwn(valueObject, returnType)) {
+ return valueObject[returnType as keyof ValueObject];
+ }
+ if (returnType == 'object') {
+ return valueObject;
+ }
+ console.error('returnType not found!', returnType, power);
+ };
+ });
+
+ /**
+ * Get the part of the home consumption that is not measured by a
+ * component.
+ * @param returnType type of return value, 'textValue', 'value', 'scaledValue', 'scaledUnit' or 'object'
+ * @returns string | number | ValueObject
+ */
+ const unmeasuredHomeConsumptionPower = computed(() => {
+ return (returnType: string = 'textValue') => {
+ const power = getValue.value(
+ 'openWB/counter/set/unmeasured_home_consumption',
+ ) as number | undefined;
+ const valueObject = getValueObject.value(power);
+ if (Object.hasOwn(valueObject, returnType)) {
+ return valueObject[returnType as keyof ValueObject];
+ }
+ if (returnType == 'object') {
+ return valueObject;
+ }
+ console.error('returnType not found!', returnType, power);
+ };
+ });
+
/**
* Get pv power
* @param returnType type of return value, 'textValue', 'value', 'scaledValue', 'scaledUnit' or 'object'
@@ -4753,6 +4875,10 @@ export const useMqttStore = defineStore('mqtt', () => {
// Home data
homePower,
homeDailyYield,
+ inHomeConsumption,
+ notInHomeConsumption,
+ notInHomeConsumptionPower,
+ unmeasuredHomeConsumptionPower,
// PV data
pvConfigured,
hybridInverters,