{{ props.item.title }}
{{ props.item.title }}
@@ -88,6 +93,12 @@
import { computed } from 'vue';
import { useMqttStore } from 'src/stores/mqtt-store';
import type { DailyTotalsItem } from 'src/components/models/daily-totals-model';
+import BatteryIcon from 'src/assets/icons/owbBattery_2.svg?component';
+import GridIcon from 'src/assets/icons/owbGrid.svg?component';
+import PvIcon from 'src/assets/icons/owbPV.svg?component';
+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';
const mqttStore = useMqttStore();
@@ -135,6 +146,15 @@ const arrowDirection = (id: string) => {
return { noCurrent, rotate180 };
};
+
+const iconMap = {
+ grid: GridIcon,
+ battery: BatteryIcon,
+ chargepoint: ChargePointIcon,
+ vehicle: VehicleIcon,
+ pv: PvIcon,
+ house: HouseIcon,
+};
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 b0fafafd55..d9ea0363d9 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
@@ -1,3 +1,5 @@
+import { Component } from 'vue';
+
export interface SvgSize {
xMin: number;
xMax: number;
@@ -31,5 +33,6 @@ export interface FlowComponent {
label: string[];
powerValue?: number;
soc?: number;
- icon: string;
+ iconComponent?: Component;
+ iconColor?: string | null;
}
diff --git a/packages/modules/web_themes/koala/source/src/components/charts/historyChart/HistoryChart.vue b/packages/modules/web_themes/koala/source/src/components/charts/historyChart/HistoryChart.vue
index d9b668af12..35a1c38ec4 100644
--- a/packages/modules/web_themes/koala/source/src/components/charts/historyChart/HistoryChart.vue
+++ b/packages/modules/web_themes/koala/source/src/components/charts/historyChart/HistoryChart.vue
@@ -101,6 +101,10 @@ const selectedData = computed((): GraphDataPoint[] => {
});
const chargePointIds = computed(() => mqttStore.chargePointIds);
+const gridId = computed(() => mqttStore.getGridId);
+const pvColor = computed(() => mqttStore.pvAggregateColor);
+const batteryColor = computed(() => mqttStore.batteryAggregateColor);
+
const chargePointNames = computed(() => mqttStore.chargePointName);
const gridMeterName = computed(() => {
@@ -126,15 +130,27 @@ const getGlobalColor = (name: string, fallback?: string) => {
return fromRoot || fallback;
};
+const hexColorToRgba = (hex: string, opacity = 1) => {
+ hex = hex.replace('#', '');
+ const num = parseInt(hex, 16);
+ const r = (num >> 16) & 255;
+ const g = (num >> 8) & 255;
+ const b = num & 255;
+ return `rgba(${r}, ${g}, ${b}, ${opacity})`;
+};
+
const secondaryCounterDatasets = computed(() =>
mqttStore.getSecondaryCounterIds
.map((id) => {
+ const baseColor =
+ mqttStore.getSecondaryCounterColor(id) ||
+ getGlobalColor('--q-secondary-counter-stroke');
return {
label: mqttStore.getComponentName(id),
category: 'component',
unit: 'kW',
- borderColor: getGlobalColor('--q-secondary-counter-stroke'),
- backgroundColor: getGlobalColor('--q-secondary-counter-fill'),
+ borderColor: baseColor,
+ backgroundColor: hexColorToRgba(baseColor, 0.1),
data: selectedData.value.map((item) => ({
x: item.timestamp * 1000,
y: item[`counter${id}-power`] ?? 0,
@@ -151,23 +167,29 @@ const secondaryCounterDatasets = computed(() =>
);
const chargePointDatasets = computed(() =>
- chargePointIds.value.map((cpId) => ({
- label: `${chargePointNames.value(cpId)}`,
- category: 'chargepoint',
- unit: 'kW',
- borderColor: getGlobalColor('--q-charge-point-stroke'),
- backgroundColor: getGlobalColor('--q-charge-point-fill'),
- data: selectedData.value.map((item) => ({
- x: item.timestamp * 1000,
- y: item[`cp${cpId}-power`] || 0,
- })),
- borderWidth: 2,
- pointRadius: 0,
- pointHoverRadius: 4,
- pointHitRadius: 5,
- fill: true,
- yAxisID: 'y',
- })),
+ chargePointIds.value.map((cpId) => {
+ const baseColor =
+ mqttStore.chargePointUserDefinedColor(cpId) ||
+ getGlobalColor('--q-charge-point-stroke');
+
+ return {
+ label: `${chargePointNames.value(cpId)}`,
+ category: 'chargepoint',
+ unit: 'kW',
+ borderColor: baseColor,
+ backgroundColor: hexColorToRgba(baseColor, 0.1),
+ data: selectedData.value.map((item) => ({
+ x: item.timestamp * 1000,
+ y: item[`cp${cpId}-power`] || 0,
+ })),
+ borderWidth: 2,
+ pointRadius: 0,
+ pointHoverRadius: 4,
+ pointHitRadius: 5,
+ fill: true,
+ yAxisID: 'y',
+ };
+ }),
);
const vehicleDatasets = computed(() =>
@@ -175,11 +197,14 @@ const vehicleDatasets = computed(() =>
.map((vehicle) => {
const socKey = `ev${vehicle.id}-soc` as keyof GraphDataPoint;
if (selectedData.value.some((item) => socKey in item)) {
+ const baseColor =
+ mqttStore.vehicleUserDefinedColor(vehicle.id) ||
+ getGlobalColor('--q-vehicle-stroke');
return {
label: `${vehicle.name} SoC`,
category: 'vehicle',
unit: '%',
- borderColor: '#9F8AFF',
+ borderColor: baseColor,
borderWidth: 2,
borderDash: [10, 5],
pointRadius: 0,
@@ -230,12 +255,15 @@ const chartLabels = computed(() => {
const lineChartData = computed(() => {
let datasets = [];
if (gridMeterName.value !== undefined) {
+ const baseColor =
+ mqttStore.getGridComponentColor(gridId.value) ||
+ getGlobalColor('--q-grid-stroke');
datasets.push({
label: gridMeterName.value,
category: 'component',
unit: 'kW',
- borderColor: getGlobalColor('--q-grid-stroke'),
- backgroundColor: getGlobalColor('--q-grid-fill'),
+ borderColor: baseColor,
+ backgroundColor: hexColorToRgba(baseColor, 0.1),
data: selectedData.value.map((item) => ({
x: item.timestamp * 1000,
y: item.grid,
@@ -269,12 +297,15 @@ const lineChartData = computed(() => {
}
datasets.push(...secondaryCounterDatasets.value);
if (mqttStore.getPvConfigured) {
+ const baseColor =
+ pvColor.value ||
+ getGlobalColor('--q-pv-stroke');
datasets.push({
label: 'PV ges.',
category: 'component',
unit: 'kW',
- borderColor: getGlobalColor('--q-pv-stroke'),
- backgroundColor: getGlobalColor('--q-pv-fill'),
+ borderColor: baseColor,
+ backgroundColor: hexColorToRgba(baseColor, 0.1),
data: selectedData.value.map((item) => ({
x: item.timestamp * 1000,
y: item['pv-all'],
@@ -288,13 +319,16 @@ const lineChartData = computed(() => {
});
}
if (mqttStore.batteryConfigured) {
+ const baseColor =
+ batteryColor.value ||
+ getGlobalColor('--q-battery-stroke');
datasets.push(
{
label: 'Speicher ges.',
category: 'component',
unit: 'kW',
- borderColor: getGlobalColor('--q-battery-stroke'),
- backgroundColor: getGlobalColor('--q-battery-fill'),
+ borderColor: baseColor,
+ backgroundColor: hexColorToRgba(baseColor, 0.1),
data: selectedData.value.map((item) => ({
x: item.timestamp * 1000,
y: item['bat-all-power'],
diff --git a/packages/modules/web_themes/koala/source/src/components/models/daily-totals-model.ts b/packages/modules/web_themes/koala/source/src/components/models/daily-totals-model.ts
index a2d4738319..453ab96cb3 100644
--- a/packages/modules/web_themes/koala/source/src/components/models/daily-totals-model.ts
+++ b/packages/modules/web_themes/koala/source/src/components/models/daily-totals-model.ts
@@ -2,6 +2,7 @@ export interface DailyTotalsItem {
id: string;
title: string;
icon: string;
+ level: 'primary' | 'secondary';
soc?: number;
power?: string;
powerValue?: number;
@@ -14,4 +15,5 @@ export interface DailyTotalsItem {
rightLabel?: string;
rightValue?: string;
arrow?: string;
+ color?: string;
}
diff --git a/packages/modules/web_themes/koala/source/src/components/models/table-model.ts b/packages/modules/web_themes/koala/source/src/components/models/table-model.ts
index 411594f309..72b0195e0d 100644
--- a/packages/modules/web_themes/koala/source/src/components/models/table-model.ts
+++ b/packages/modules/web_themes/koala/source/src/components/models/table-model.ts
@@ -5,12 +5,14 @@ export type ColumnConfiguration = {
label: string;
align?: 'left' | 'right' | 'center';
expandField?: boolean;
+ autoWidth?: boolean;
+ shrink?: boolean;
};
export interface BodySlotProps {
key: string | number;
row: T;
- cols: QTableColumn[];
+ cols: ExtendedQTableColumn[];
expand: boolean;
}
@@ -27,6 +29,7 @@ export interface ChargePointRow extends Record {
current: string;
powerColumn: '';
charged: string;
+ color: string;
}
export interface VehicleRow extends Record {
@@ -37,4 +40,10 @@ export interface VehicleRow extends Record {
plugState: boolean;
chargeState: boolean;
vehicleSocValue: string;
+ color: string;
}
+
+export type ExtendedQTableColumn = QTableColumn & {
+ autoWidth?: boolean;
+ shrink?: boolean;
+};
diff --git a/packages/modules/web_themes/koala/source/src/css/quasar.variables.scss b/packages/modules/web_themes/koala/source/src/css/quasar.variables.scss
index 7dda260791..644611bb98 100644
--- a/packages/modules/web_themes/koala/source/src/css/quasar.variables.scss
+++ b/packages/modules/web_themes/koala/source/src/css/quasar.variables.scss
@@ -49,7 +49,9 @@ $battery-fill: #ba712833;
$battery-fill-flow-diagram: #c6a583;
$charge-point-stroke: #5c93d1;
$charge-point-fill: #5c93d14d;
+$vehicle-stroke: #9F8AFF;
$charge-plan-link-button: #6b757d;
+$diagram-icon: #717171;
// Light theme (default)
:root {
--q-primary: #{$primary};
@@ -79,7 +81,9 @@ $charge-plan-link-button: #6b757d;
--q-battery-fill-flow-diagram: #{$battery-fill-flow-diagram};
--q-charge-point-stroke: #{$charge-point-stroke};
--q-charge-point-fill: #{$charge-point-fill};
+ --q-vehicle-stroke: #{$vehicle-stroke};
--q-charge-plan-link-button: #{$charge-plan-link-button};
+ --q-diagram-icon: #{$diagram-icon};
// Main background
background-color: var(--q-background-1);
@@ -299,6 +303,7 @@ $dark-charge-plan-link-button: #75787a;
--q-battery-fill: #{$battery-fill};
--q-charge-point-stroke: #{$charge-point-stroke};
--q-charge-point-fill: #{$charge-point-fill};
+ --q-vehicle-stroke: #{$vehicle-stroke};
--q-battery-fill-flow-diagram: #{$battery-fill-flow-diagram};
--q-dark-daily-totals-grid-fill: #{$dark-daily-totals-grid-fill};
--q-dark-daily-totals-grid-stroke: #{$dark-daily-totals-grid-stroke};
@@ -308,6 +313,7 @@ $dark-charge-plan-link-button: #75787a;
--q-dark-daily-totals-house-fill: #{$dark-daily-totals-house-fill};
--q-dark-daily-totals-chargepoint-fill: #{$dark-daily-totals-chargepoint-fill};
--q-charge-plan-link-button: #{$dark-charge-plan-link-button};
+ --q-diagram-icon: #{$white};
// Main background
background-color: $dark-page;
diff --git a/packages/modules/web_themes/koala/source/src/stores/mqtt-store-model.ts b/packages/modules/web_themes/koala/source/src/stores/mqtt-store-model.ts
index 015d773507..3810f16ccb 100644
--- a/packages/modules/web_themes/koala/source/src/stores/mqtt-store-model.ts
+++ b/packages/modules/web_themes/koala/source/src/stores/mqtt-store-model.ts
@@ -203,18 +203,7 @@ export interface GraphDataPoint {
[key: `ev${number}-soc`]: number | null;
}
-export interface BatteryConfiguration {
- name: string;
- info: {
- manufacturer: string;
- model: string;
- };
- type: string;
- id: number;
- configuration: object;
-}
-
-export interface CounterConfiguration {
+export interface ComponentConfiguration {
name: string;
info: {
manufacturer: string;
@@ -223,6 +212,7 @@ export interface CounterConfiguration {
type: string;
id: number;
configuration: object;
+ color: string;
}
export interface RangeValue {
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 d405a29ebb..1a45751afc 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
@@ -19,8 +19,7 @@ import type {
ScheduledChargingPlan,
ChargePointConnectedVehicleSoc,
GraphDataPoint,
- BatteryConfiguration,
- CounterConfiguration,
+ ComponentConfiguration,
ThemeConfiguration,
VehicleActivePlan,
TimeChargingPlan,
@@ -647,6 +646,12 @@ export const useMqttStore = defineStore('mqtt', () => {
const path = objectPath.split('.');
for (let i = 0; i < path.length; i++) {
if (!Object.hasOwn(topicObject, path[i])) {
+ if (defaultValue !== undefined) {
+ // expected missing optional value - no error
+ console.debug('optional path not found', topicObject, path[i]);
+ return defaultValue;
+ }
+ // real error case
console.error('path not found', topicObject, path[i]);
return defaultValue;
}
@@ -757,6 +762,20 @@ export const useMqttStore = defineStore('mqtt', () => {
);
});
+ /**
+ * Get component attributes
+ * @param componentId component ID
+ * @returns ComponentConfiguration | undefined
+ */
+ const getComponentAttributes = computed(() => {
+ return (componentId: number): ComponentConfiguration | undefined => {
+ const configurations = getWildcardValues.value(
+ `openWB/system/device/+/component/${componentId}/config`,
+ ) as Record;
+ return Object.values(configurations)[0];
+ };
+ });
+
/**
* Check if user management is active
* Defaults to true if the value is not set as this may be due to insufficient permissions
@@ -1215,6 +1234,23 @@ export const useMqttStore = defineStore('mqtt', () => {
};
});
+ /**
+ * Get the charge point user defined color identified by the charge point id
+ * @param chargePointId charge point id
+ * @returns string | null
+ */
+ const chargePointUserDefinedColor = computed(() => {
+ return (chargePointId: number): string | null => {
+ const DEFAULT_COLOR = '#007bff';
+ const color = getValue.value(
+ `openWB/chargepoint/${chargePointId}/config`,
+ 'color',
+ null,
+ ) as string | null;
+ return resolveComponentColor(color, DEFAULT_COLOR);
+ };
+ });
+
/**
* trigger a force SOC update for the connected vehicle
* @param chargePointId charge point id
@@ -2644,7 +2680,7 @@ export const useMqttStore = defineStore('mqtt', () => {
return (batteryId: number): string => {
const configurations = getWildcardValues.value(
`openWB/system/device/+/component/${batteryId}/config`,
- ) as { [key: string]: BatteryConfiguration };
+ ) as { [key: string]: ComponentConfiguration };
if (Object.keys(configurations).length === 0) {
return undefined;
}
@@ -2869,6 +2905,32 @@ export const useMqttStore = defineStore('mqtt', () => {
});
};
+ /**
+ * Get the battery color if exactly one battery is configured
+ * @param batteryId battery id
+ * @returns string | null
+ */
+ const batteryAggregateColor = computed(() => {
+ const ids = batteryIds.value;
+ if (ids.length === 1) {
+ return batteryColor.value(ids[0]);
+ }
+ return null;
+ });
+
+ /**
+ * Get the battery color identified by the battery id
+ * @param batteryId battery id
+ * @returns string
+ */
+ const batteryColor = computed(() => {
+ return (batteryId: number): string => {
+ const DEFAULT_COLOR = '#ffc107';
+ const config = getComponentAttributes.value(batteryId);
+ return resolveComponentColor(config?.color, DEFAULT_COLOR);
+ };
+ });
+
////////////////////////////// vehicle data ////////////////////////////////
/**
@@ -2925,6 +2987,23 @@ export const useMqttStore = defineStore('mqtt', () => {
};
});
+ /**
+ * Get the vehicle user defined color identified by the vehicle id
+ * @param vehicleId vehicle id
+ * @returns string | null
+ */
+ const vehicleUserDefinedColor = computed(() => {
+ return (vehicleId: number): string | null => {
+ const DEFAULT_COLOR = '#17a2b8';
+ const color = getValue.value(
+ `openWB/vehicle/${vehicleId}/color`,
+ undefined,
+ null,
+ ) as string | null;
+ return resolveComponentColor(color, DEFAULT_COLOR);
+ };
+ });
+
/**
* Get vehicle SoC value identified by the vehicle id
* @param vehicleId vehicle id
@@ -3699,6 +3778,19 @@ export const useMqttStore = defineStore('mqtt', () => {
return getAllCounterIds.value.filter((id) => id !== rootCounter);
});
+ /**
+ * Get the secondary counter color identified by the component ID
+ * @param componentId component ID
+ * @returns string | null
+ */
+ const getSecondaryCounterColor = computed(() => {
+ return (componentId: number) => {
+ const DEFAULT_COLOR = '#dc3545';
+ const color = getComponentAttributes.value(componentId)?.color;
+ return resolveComponentColor(color, DEFAULT_COLOR);
+ };
+ });
+
/**
* Get the power meter(counter) name identified by the Grid ID
* @param counterId counter ID
@@ -3708,11 +3800,24 @@ export const useMqttStore = defineStore('mqtt', () => {
return (componentId: number): string => {
const configurations = getWildcardValues.value(
`openWB/system/device/+/component/${componentId}/config`,
- ) as { [key: string]: CounterConfiguration };
+ ) as { [key: string]: ComponentConfiguration };
return Object.values(configurations)[0]?.name || undefined;
};
});
+ /**
+ * Get grid component color
+ * @param componentId component ID
+ * @returns GridConfiguration | null
+ */
+ const getGridComponentColor = computed(() => {
+ return (componentId: number) => {
+ const DEFAULT_COLOR = '#dc3545';
+ const color = getComponentAttributes.value(componentId)?.color;
+ return resolveComponentColor(color, DEFAULT_COLOR);
+ };
+ });
+
/**
* Get counter power identified by root grid counter in component hierarchy or counterId
* @param returnType type of return value, 'textValue', 'value', 'scaledValue', 'scaledUnit' or 'object'
@@ -3857,6 +3962,31 @@ export const useMqttStore = defineStore('mqtt', () => {
);
});
+ /**
+ * Get the pv color identified by the inverter id
+ * @param inverterId inverter id
+ * @returns string | null
+ */
+ const pvColor = computed(() => {
+ return (inverterId: number): string | null => {
+ const DEFAULT_COLOR = '#28a745';
+ const config = getComponentAttributes.value(inverterId);
+ return resolveComponentColor(config?.color, DEFAULT_COLOR);
+ };
+ });
+
+ /**
+ * Get the pv color for the pv if exactly one inverter is configured
+ * @returns string | null
+ */
+ const pvAggregateColor = computed((): string | null => {
+ const ids = getObjectIds.value('inverter');
+ if (ids.length === 1) {
+ return pvColor.value(ids[0]);
+ }
+ return null;
+ });
+
/**
* Get pv power
* @param returnType type of return value, 'textValue', 'value', 'scaledValue', 'scaledUnit' or 'object'
@@ -3942,6 +4072,15 @@ export const useMqttStore = defineStore('mqtt', () => {
};
});
+ /* helpers */
+ const resolveComponentColor = (
+ color: string | null | undefined,
+ defaultColor: string,
+ ) => {
+ if (!color || color === defaultColor) return null;
+ return color;
+ };
+
// exports
return {
topics,
@@ -3962,6 +4101,7 @@ export const useMqttStore = defineStore('mqtt', () => {
themeConfiguration,
systemDateTime,
dataProtectionAcknowledged,
+ getComponentAttributes,
// security settings
userManagementActive,
accessAllowed,
@@ -3986,6 +4126,7 @@ export const useMqttStore = defineStore('mqtt', () => {
chargePointChargingCurrent,
chargePointStateMessage,
chargePointFaultState,
+ chargePointUserDefinedColor,
chargePointFaultMessage,
temporaryChargeModeActive,
chargePointChargeType,
@@ -4027,6 +4168,7 @@ export const useMqttStore = defineStore('mqtt', () => {
vehicleInfo,
vehicleConnectionState,
vehicleSocType,
+ vehicleUserDefinedColor,
vehicleSocValue,
vehicleSocManualValue,
vehicleForceSocUpdate,
@@ -4086,11 +4228,15 @@ export const useMqttStore = defineStore('mqtt', () => {
batteryTotalPower,
batteryChargePriorityRange,
batteryMode,
+ batteryAggregateColor,
+ batteryColor,
// Grid data
getGridId,
getAllCounterIds,
getSecondaryCounterIds,
+ getSecondaryCounterColor,
getComponentName,
+ getGridComponentColor,
getCounterPower,
counterDailyImported,
counterDailyExported,
@@ -4099,6 +4245,8 @@ export const useMqttStore = defineStore('mqtt', () => {
homeDailyYield,
// PV data
getPvConfigured,
+ pvAggregateColor,
+ pvColor,
getPvPower,
pvDailyExported,
// Chart data
@@ -4106,5 +4254,7 @@ export const useMqttStore = defineStore('mqtt', () => {
// electricity tariff provider
etProviderConfigured,
etPrices,
+ // helpers
+ resolveComponentColor,
};
});
diff --git a/packages/modules/web_themes/koala/source/src/types/svg.d.ts b/packages/modules/web_themes/koala/source/src/types/svg.d.ts
new file mode 100644
index 0000000000..fc4161d78b
--- /dev/null
+++ b/packages/modules/web_themes/koala/source/src/types/svg.d.ts
@@ -0,0 +1,5 @@
+declare module '*.svg?component' {
+ import type { Component } from 'vue';
+ const component: Component;
+ export default component;
+}