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 @@ + + + + + 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,