Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Original file line number Diff line number Diff line change
Expand Up @@ -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();
Expand Down Expand Up @@ -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);
Expand Down Expand Up @@ -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(() => {
Expand Down Expand Up @@ -380,25 +460,27 @@ const svgComponents = computed((): FlowComponent[] => {
powerValue: Number(homePower.value.value),
iconComponent: HouseIcon,
iconColor: 'var(--q-home-stroke)',
showInfo: homeHasDetails.value,
});
}

if (showConsumerPower.value) {
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,
});
}

Expand Down Expand Up @@ -873,6 +955,8 @@ const labelClipPath = computed(() => {
</text>
<g
:transform="`translate(${svgSize.circleRadius - svgRectWidth / 2}, 0)`"
:class="{ 'cursor-pointer': component.showInfo }"
@click="openDetails($event, component)"
>
<circle
cx="0"
Expand Down Expand Up @@ -902,10 +986,37 @@ const labelClipPath = computed(() => {
:style="{ color: component.iconColor }"
/>
</g>
<!-- info badge: sits on the top left edge of the icon circle -->
<g
v-if="component.showInfo"
class="info-badge"
:transform="`translate(${-infoBadgeOffset}, ${-infoBadgeOffset})`"
>
<circle
cx="0"
cy="0"
:r="infoBadgeRadius"
filter="url(#flow-box-shadow)"
/>
<g
:transform="`translate(${-infoIconSize / 2}, ${-infoIconSize / 2})`"
>
<InfoIcon
class="info-icon"
:width="infoIconSize"
:height="infoIconSize"
/>
</g>
</g>
</g>
</g>
</g>
</svg>
<HomeConsumptionDetailsDialog
v-model="detailsVisible"
:scope="detailsScope"
:title="detailsTitle"
/>
</div>
</template>

Expand Down Expand Up @@ -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);
Expand Down Expand Up @@ -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);
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,144 @@
<template>
<q-dialog v-model="visible">
<q-card class="card-width">
<q-card-section>
<div class="row no-wrap items-center">
<InfoIcon class="title-icon q-mr-md" />
<div class="text-h6 q-pr-sm">{{ title }}</div>
<q-space />
<q-btn
icon="close"
flat
round
dense
v-close-popup
class="close-btn"
/>
</div>
</q-card-section>
<q-separator />
<q-card-section class="q-pa-none">
<q-list dense class="q-pt-sm">
<q-item v-for="item in items" :key="item.key">
<q-item-section avatar>
<component
:is="item.icon"
class="item-icon"
:style="{ color: item.color }"
/>
</q-item-section>
<q-item-section>
<div class="row no-wrap items-center">
<div class="col">{{ item.name }}</div>
<div class="q-pl-md text-no-wrap">{{ item.power }}</div>
</div>
</q-item-section>
</q-item>
</q-list>
</q-card-section>
</q-card>
</q-dialog>
</template>

<script setup lang="ts">
import { computed } from 'vue';
import type { Component } from 'vue';
import { useMqttStore } from 'src/stores/mqtt-store';
import ConsumerIcon from 'src/assets/icons/owbConsumer.svg?component';
import CounterIcon from 'src/assets/icons/owbCounter.svg?component';
import HouseIcon from 'src/assets/icons/owbHouse.svg?component';
import InfoIcon from 'src/assets/icons/owbInformation.svg?component';

defineOptions({ name: 'HomeConsumptionDetailsDialog' });

interface DetailsItem {
key: string;
name: string;
icon: Component;
color: string;
power: string;
}

const props = defineProps<{
modelValue: boolean;
scope: 'inHome' | 'notInHome';
title: string;
}>();

const emit = defineEmits<{
'update:modelValue': [value: boolean];
}>();

const mqttStore = useMqttStore();

const visible = computed({
get: () => props.modelValue,
set: (value) => emit('update:modelValue', value),
});

const items = computed((): DetailsItem[] => {
const { consumerIds, counterIds } =
props.scope === 'inHome'
? mqttStore.inHomeConsumption
: mqttStore.notInHomeConsumption;
const unmeasured =
props.scope === 'inHome' &&
mqttStore.unmeasuredHomeConsumptionPower('value') !== undefined
? [
{
key: 'unmeasured',
name: 'Nicht erfasst',
icon: HouseIcon,
color: 'var(--q-home-stroke)',
power: mqttStore.unmeasuredHomeConsumptionPower() as string,
},
]
: [];
return [
...consumerIds.map((id) => ({
key: `consumer-${id}`,
name: mqttStore.consumerName(id) || `Verbraucher ${id}`,
icon: ConsumerIcon,
color: mqttStore.consumerColor(id) || 'var(--q-consumer)',
power: mqttStore.consumerPower(id, 'textValue') as string,
})),
...counterIds.map((id) => ({
key: `counter-${id}`,
name: mqttStore.componentName(id) || `Zähler ${id}`,
icon: CounterIcon,
color:
mqttStore.secondaryCounterColor(id) ||
'var(--q-secondary-counter-stroke)',
power: mqttStore.counterPower('textValue', id) as string,
})),
...unmeasured,
];
});
</script>

<style lang="scss" scoped>
.card-width {
width: 400px;
max-width: 90vw;
}
.close-btn {
height: 2.5em;
width: 2.5em;
}
.title-icon,
.item-icon {
width: 28px;
height: 28px;
}
.title-icon {
flex-shrink: 0;
color: var(--q-primary);
}
.q-item__section--avatar {
min-width: 0;
}

.q-list {
background-color: transparent;
}
</style>
Original file line number Diff line number Diff line change
Expand Up @@ -35,4 +35,5 @@ export interface FlowComponent {
soc?: number;
iconComponent?: Component;
iconColor?: string;
showInfo?: boolean;
}
Loading
Loading