diff --git a/packages/modules/web_themes/koala/source/src/components/BaseTable.vue b/packages/modules/web_themes/koala/source/src/components/BaseTable.vue index 8d4391321c..902af8e808 100644 --- a/packages/modules/web_themes/koala/source/src/components/BaseTable.vue +++ b/packages/modules/web_themes/koala/source/src/components/BaseTable.vue @@ -5,6 +5,7 @@ :rows="mappedRows" :columns="mappedColumns" row-key="id" + v-model:expanded="expanded" :filter="filterModel" :filter-method="customFilterMethod" virtual-scroll @@ -16,7 +17,8 @@ :pagination="{ rowsPerPage: 0 }" hide-bottom > - diff --git a/packages/modules/web_themes/koala/source/src/components/charts/historyChart/HistoryChartLegend.vue b/packages/modules/web_themes/koala/source/src/components/charts/historyChart/HistoryChartLegend.vue new file mode 100644 index 0000000000..393509c390 --- /dev/null +++ b/packages/modules/web_themes/koala/source/src/components/charts/historyChart/HistoryChartLegend.vue @@ -0,0 +1,147 @@ + + + + + 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 new file mode 100644 index 0000000000..411594f309 --- /dev/null +++ b/packages/modules/web_themes/koala/source/src/components/models/table-model.ts @@ -0,0 +1,40 @@ +import { QTableColumn } from 'quasar'; + +export type ColumnConfiguration = { + field: string; + label: string; + align?: 'left' | 'right' | 'center'; + expandField?: boolean; +}; + +export interface BodySlotProps { + key: string | number; + row: T; + cols: QTableColumn[]; + expand: boolean; +} + +export interface ChargePointRow extends Record { + id: number; + name: string | undefined; + vehicle: string; + plugged: boolean; + chargeMode: string | undefined; + timeCharging: boolean | undefined; + soc: string; + power: string; + phaseNumber: number; + current: string; + powerColumn: ''; + charged: string; +} + +export interface VehicleRow extends Record { + id: number; + name: string; + manufacturer: string; + model: string; + plugState: boolean; + chargeState: boolean; + vehicleSocValue: string; +} diff --git a/packages/modules/web_themes/koala/source/src/composables/useChargeModes.ts b/packages/modules/web_themes/koala/source/src/composables/useChargeModes.ts index 340cd59a79..60e9238e0f 100644 --- a/packages/modules/web_themes/koala/source/src/composables/useChargeModes.ts +++ b/packages/modules/web_themes/koala/source/src/composables/useChargeModes.ts @@ -3,7 +3,7 @@ export const useChargeModes = () => { { value: 'instant_charging', label: 'Sofort', color: 'negative' }, { value: 'pv_charging', label: 'PV', color: 'positive' }, { value: 'scheduled_charging', label: 'Ziel', color: 'primary' }, - { value: 'eco_charging', label: 'Eco', color: 'secondary' }, + { value: 'eco_charging', label: 'Eco', color: 'accent' }, { value: 'stop', label: 'Stop', color: 'light' }, ]; return { 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 001ade7642..57aa50cd25 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 @@ -191,6 +191,26 @@ $battery: #ba7128; scrollbar-color: var(--q-primary) var(--q-secondary); } } + + //table padding + .q-table th, + .q-table td { + padding: 2px 6px !important; + } + + .q-table th:first-child, + .q-table td:first-child { + padding-left: 12px !important; + } + + .q-table th:last-child, + .q-table td:last-child { + padding-right: 12px !important; + } + // Scroll area styling + .q-scrollarea { + border: 1px solid var(--q-secondary) !important; + } } // Dark Theme Base Colors $dark-page: #000000; // This overrides Quasar's default dark page color @@ -376,4 +396,8 @@ $dark-tab-icon: #d7d9e0; scrollbar-color: var(--q-primary) var(--q-secondary); } } + + .q-scrollarea { + border: 1px solid var(--q-secondary) !important; + } } 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 906c80e817..42552ae729 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 @@ -122,12 +122,12 @@ export interface Vehicle { id: number; name: string; } -export interface vehicleInfo { +export interface VehicleInfo { manufacturer: string; model: string; } -export interface vehicleSocModule { +export interface VehicleSocModuleConfig { name?: string; type: string | null; official?: 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 c9db6e7f23..a3b55345cc 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 @@ -15,8 +15,8 @@ import type { ValueObject, ChargePointConnectedVehicleInfo, Vehicle, - vehicleInfo, - vehicleSocModule, + VehicleInfo, + VehicleSocModuleConfig, ScheduledChargingPlan, ChargePointConnectedVehicleSoc, GraphDataPoint, @@ -491,6 +491,7 @@ export const useMqttStore = defineStore('mqtt', () => { * @param scale flag to scale the value, default is true * @param inverted flag to invert the value, default is false * @param defaultString default string to use, default is '---' + * @param decimalPlaces number of decimal places to use, default is 0 or 2 for scaled values * @returns object */ const getValueObject = computed(() => { @@ -501,6 +502,7 @@ export const useMqttStore = defineStore('mqtt', () => { scale: boolean = true, inverted: boolean = false, defaultString: string = '---', + decimalPlaces: number = 0, ) => { let scaled = false; let scaledValue = value; @@ -534,9 +536,16 @@ export const useMqttStore = defineStore('mqtt', () => { break; } } + let outputDecimalPlaces = 0; + if (scaled) { + outputDecimalPlaces = decimalPlaces > 0 ? decimalPlaces : 2; + } else { + const hasDecimalPlaces = scaledValue !== Math.floor(scaledValue); + outputDecimalPlaces = hasDecimalPlaces ? decimalPlaces : 0; + } textValue = scaledValue.toLocaleString(undefined, { - minimumFractionDigits: scaled ? 2 : 0, - maximumFractionDigits: scaled ? 2 : 0, + minimumFractionDigits: outputDecimalPlaces, + maximumFractionDigits: outputDecimalPlaces, }); } return { @@ -806,16 +815,25 @@ export const useMqttStore = defineStore('mqtt', () => { }; }); - /** - * Get the charge point charging current identified by the charge point id - */ const chargePointChargingCurrent = computed(() => { - return (chargePointId: number) => { - return ( + return (chargePointId: number, returnType: string = 'textValue') => { + const current = (getValue.value( `openWB/chargepoint/${chargePointId}/set/current`, - ) as number) || 0 + ) as number) || 0; + const valueObject = getValueObject.value( + current, + 'A', + '', + true, + false, + '---', + 2, ); + if (Object.hasOwn(valueObject, returnType)) { + return valueObject[returnType as keyof ValueObject]; + } + console.error('returnType not found!', returnType, current); }; }); @@ -941,6 +959,46 @@ export const useMqttStore = defineStore('mqtt', () => { }); }; + /** + * Get boolean value for DC charging enabled / disabled + * @returns boolean + */ + const dcChargingEnabled = computed(() => { + return (getValue.value('openWB/optional/dc_charging') as boolean) || 0; + }); + + /** + * Get or set the charge point connected vehicle instant charging DC power identified by the charge point id + * @param chargePointId charge point id + * @returns number + */ + const chargePointConnectedVehicleInstantDcChargePower = ( + chargePointId: number, + ) => { + return computed({ + get() { + const dcCurrent = + chargePointConnectedVehicleChargeTemplate(chargePointId).value + ?.chargemode?.instant_charging?.dc_current; + if (dcCurrent !== undefined) { + return (dcCurrent * 3 * 230) / 1000; + } else { + return 0; + } + }, + set(newValue: number) { + console.debug('set instant charging power', newValue, chargePointId); + const newPower = (newValue * 1000) / 230 / 3; + return updateTopic( + `openWB/chargepoint/${chargePointId}/set/charge_template`, + newPower, + 'chargemode.instant_charging.dc_current', + true, + ); + }, + }); + }; + /** * Get or set the charge point connected vehicle instant charging phases identified by the charge point id * @param chargePointId charge point id @@ -1052,7 +1110,7 @@ export const useMqttStore = defineStore('mqtt', () => { * @param chargePointId charge point id * @returns object | undefined */ - const chargePointConnectedVehiclePVChargeMinCurrent = ( + const chargePointConnectedVehiclePvChargeMinCurrent = ( chargePointId: number, ) => { return computed({ @@ -1072,12 +1130,44 @@ export const useMqttStore = defineStore('mqtt', () => { }); }; + /** + * Get or set the charge point connected vehicle PV charging DC power identified by the charge point id + * @param chargePointId charge point id + * @returns number + */ + const chargePointConnectedVehiclePvDcChargePower = ( + chargePointId: number, + ) => { + return computed({ + get() { + const dcMinCurrent = + chargePointConnectedVehicleChargeTemplate(chargePointId).value + ?.chargemode?.pv_charging?.dc_min_current; + if (dcMinCurrent !== undefined) { + return (dcMinCurrent * 3 * 230) / 1000; + } else { + return 0; + } + }, + set(newValue: number) { + console.debug('set instant charging power', newValue, chargePointId); + const newPower = (newValue * 1000) / 230 / 3; + return updateTopic( + `openWB/chargepoint/${chargePointId}/set/charge_template`, + newPower, + 'chargemode.pv_charging.dc_min_current', + true, + ); + }, + }); + }; + /** * Get or set the charge point connected vehicle pv min SoC identified by the charge point id * @param chargePointId charge point id * @returns object | undefined */ - const chargePointConnectedVehiclePVChargeMinSoc = (chargePointId: number) => { + const chargePointConnectedVehiclePvChargeMinSoc = (chargePointId: number) => { return computed({ get() { return chargePointConnectedVehicleChargeTemplate(chargePointId).value @@ -1100,7 +1190,7 @@ export const useMqttStore = defineStore('mqtt', () => { * @param chargePointId charge point id * @returns object | undefined */ - const chargePointConnectedVehiclePVChargeMinSocCurrent = ( + const chargePointConnectedVehiclePvChargeMinSocCurrent = ( chargePointId: number, ) => { return computed({ @@ -1256,7 +1346,7 @@ export const useMqttStore = defineStore('mqtt', () => { * @param chargePointId charge point id * @returns object | undefined */ - const chargePointConnectedVehiclePVChargeFeedInLimit = ( + const chargePointConnectedVehiclePvChargeFeedInLimit = ( chargePointId: number, ) => { return computed({ @@ -1301,6 +1391,38 @@ export const useMqttStore = defineStore('mqtt', () => { }); }; + /** + * Get or set the charge point connected vehicle eco charging power identified by the charge point id + * @param chargePointId charge point id + * @returns number + */ + const chargePointConnectedVehicleEcoChargeDcPower = ( + chargePointId: number, + ) => { + return computed({ + get() { + const dcCurrent = + chargePointConnectedVehicleChargeTemplate(chargePointId).value + ?.chargemode?.eco_charging?.dc_current; + if (dcCurrent !== undefined) { + return (dcCurrent * 3 * 230) / 1000; + } else { + return 0; + } + }, + set(newValue: number) { + console.debug('set instant charging power', newValue, chargePointId); + const newPower = (newValue * 1000) / 230 / 3; + return updateTopic( + `openWB/chargepoint/${chargePointId}/set/charge_template`, + newPower, + 'chargemode.eco_charging.dc_current', + true, + ); + }, + }); + }; + /** * Get or set the charge point connected vehicle eco charging phases identified by the charge point id * @param chargePointId charge point id @@ -1638,7 +1760,7 @@ export const useMqttStore = defineStore('mqtt', () => { /** * Get the battery power identified by the battery point id - * @param batteryId battery point id + * @param batteryId battery ID * @param returnType type of return value, 'textValue', 'absoluteTextValue', 'value', 'scaledValue', 'scaledUnit' or 'object' * @returns string | number | ValueObject */ @@ -1668,7 +1790,7 @@ export const useMqttStore = defineStore('mqtt', () => { /** * Get the battery daily imported energy of a given battery id - * @param batteryId charge point id + * @param batteryId battery ID * @param returnType type of return value, 'textValue', 'value', 'scaledValue', 'scaledUnit' or 'object' * @returns string | number | ValueObject */ @@ -1693,7 +1815,7 @@ export const useMqttStore = defineStore('mqtt', () => { /** * Get the battery daily exported energy of a given battery id - * @param batteryId charge point id + * @param batteryId battery ID * @param returnType type of return value, 'textValue', 'value', 'scaledValue', 'scaledUnit' or 'object' * @returns string | number | ValueObject */ @@ -1855,21 +1977,21 @@ export const useMqttStore = defineStore('mqtt', () => { */ const vehicleInfo = computed(() => { return (vehicleId: number) => { - return getValue.value(`openWB/vehicle/${vehicleId}/info`) as vehicleInfo; + return getValue.value(`openWB/vehicle/${vehicleId}/info`) as VehicleInfo; }; }); /** - * Get vehicle SoC module name identified by the vehicle id + * Get vehicle SoC module configuration identified by the vehicle id * @param vehicleId vehicle id - * @returns string + * @returns vehicleSocModule */ - const vehicleSocModuleName = computed(() => { + const vehicleSocModule = computed(() => { return (vehicleId: number) => { const socModule = getValue.value( `openWB/vehicle/${vehicleId}/soc_module/config`, - ) as vehicleSocModule; - return socModule?.name; + ) as VehicleSocModuleConfig; + return socModule; }; }); @@ -1886,6 +2008,52 @@ export const useMqttStore = defineStore('mqtt', () => { }; }); + /** + * Get or set the manual SoC by vehicle id + * @param vehicleId vehicle id + * @param chargePointId charge point id + * @returns number | undefined + */ + const vehicleSocManualValue = ( + vehicleId: number | undefined, + chargePointId?: number, + ) => { + return computed({ + get() { + const topic = `openWB/vehicle/${vehicleId}/soc_module/calculated_soc_state`; + const socState = getValue.value(topic) as + | CalculatedSocState + | undefined; + return socState?.manual_soc ?? socState?.soc_start ?? 0; + }, + set(newValue: number) { + doPublish( + `openWB/set/vehicle/${vehicleId}/soc_module/calculated_soc_state/manual_soc`, + newValue, + ); + // Also update the charge point connected vehicle soc to prevent long delay in display update + if (chargePointId !== undefined) { + const cpTopic = `openWB/chargepoint/${chargePointId}/get/connected_vehicle/soc`; + const cpSoc = getValue.value(cpTopic) as { soc?: number }; + if (cpSoc && cpSoc.soc !== undefined) { + updateTopic(cpTopic, newValue, 'soc', true); + } + } + }, + }); + }; + + /** + * trigger a force SOC update for the vehicle by vehicle id + */ + const vehicleForceSocUpdate = (vehicleId: number) => { + if (vehicleId !== undefined) { + const topic = `openWB/set/vehicle/${vehicleId}/get/force_soc_update`; + console.log(topic); + sendTopicToBroker(topic, 1); + } + }; + /** * Get vehicle state identified by the vehicle id * @param vehicleId vehicle id @@ -2576,24 +2744,28 @@ export const useMqttStore = defineStore('mqtt', () => { chargePointStateMessage, chargePointFaultState, chargePointFaultMessage, + dcChargingEnabled, chargePointConnectedVehicleInfo, chargePointConnectedVehicleForceSocUpdate, chargePointConnectedVehicleChargeMode, chargePointConnectedVehicleInstantChargeCurrent, + chargePointConnectedVehicleInstantDcChargePower, chargePointConnectedVehicleInstantChargePhases, chargePointConnectedVehicleInstantChargeLimit, chargePointConnectedVehicleInstantChargeLimitSoC, chargePointConnectedVehicleInstantChargeLimitEnergy, - chargePointConnectedVehiclePVChargeMinCurrent, + chargePointConnectedVehiclePvChargeMinCurrent, chargePointConnectedVehiclePvChargePhases, chargePointConnectedVehiclePvChargeLimit, chargePointConnectedVehiclePvChargeLimitSoC, chargePointConnectedVehiclePvChargeLimitEnergy, - chargePointConnectedVehiclePVChargeMinSoc, - chargePointConnectedVehiclePVChargeMinSocCurrent, + chargePointConnectedVehiclePvChargeMinSoc, + chargePointConnectedVehiclePvChargeMinSocCurrent, + chargePointConnectedVehiclePvDcChargePower, chargePointConnectedVehiclePvChargePhasesMinSoc, - chargePointConnectedVehiclePVChargeFeedInLimit, + chargePointConnectedVehiclePvChargeFeedInLimit, chargePointConnectedVehicleEcoChargeCurrent, + chargePointConnectedVehicleEcoChargeDcPower, chargePointConnectedVehicleEcoChargePhases, chargePointConnectedVehicleEcoChargeLimit, chargePointConnectedVehicleEcoChargeLimitSoC, @@ -2607,8 +2779,10 @@ export const useMqttStore = defineStore('mqtt', () => { chargePointConnectedVehicleConfig, vehicleInfo, vehicleConnectionState, - vehicleSocModuleName, + vehicleSocModule, vehicleSocValue, + vehicleSocManualValue, + vehicleForceSocUpdate, chargePointConnectedVehicleSoc, vehicleActivePlan, vehicleChargeTarget, diff --git a/packages/modules/web_themes/standard_legacy/web/index.html b/packages/modules/web_themes/standard_legacy/web/index.html index 62798e7740..9f3470d807 100644 --- a/packages/modules/web_themes/standard_legacy/web/index.html +++ b/packages/modules/web_themes/standard_legacy/web/index.html @@ -347,7 +347,12 @@
-
+
+
+
+
+
+
diff --git a/packages/modules/web_themes/standard_legacy/web/livechart.js b/packages/modules/web_themes/standard_legacy/web/livechart.js index f8dccba73f..a6dd818041 100644 --- a/packages/modules/web_themes/standard_legacy/web/livechart.js +++ b/packages/modules/web_themes/standard_legacy/web/livechart.js @@ -238,6 +238,41 @@ var chartDatasets = [ } ]; +// Generate custom legend +function generateCustomLegend(chart) { + const legendContainer = document.getElementById('custom-legend-container'); + // Clear existing legend + legendContainer.innerHTML = ''; + // Create legend items + chart.data.datasets.forEach((dataset, index) => { + if (!dataset.label) return; + const legendItem = document.createElement('div'); + legendItem.className = 'legend-item'; + if (chart.getDatasetMeta(index).hidden) { + legendItem.className += ' hidden'; + } + const colorBox = document.createElement('span'); + colorBox.className = 'legend-color-box'; + colorBox.style.backgroundColor = dataset.borderColor; + const text = document.createElement('span'); + text.textContent = dataset.label; + legendItem.appendChild(colorBox); + legendItem.appendChild(text); + // toggle visibility + legendItem.onclick = ()=> { + const item = chart.getDatasetMeta(index); + item.hidden = !item.hidden; + if (item.hidden) { + legendItem.classList.add('hidden'); + } else { + legendItem.classList.remove('hidden'); + } + chart.update(); + }; + legendContainer.appendChild(legendItem); + }); +} + function loadGraph(animationDuration = 1000) { var chartData = { @@ -295,8 +330,12 @@ function loadGraph(animationDuration = 1000) { window.myLine = new Chart(ctx, { type: 'line', plugins: [{ - afterInit: doGraphResponsive, - resize: doGraphResponsive + afterInit: (chart) => { + doGraphResponsive(chart); + generateCustomLegend(chart); + }, + resize: doGraphResponsive, + afterUpdate: generateCustomLegend }], data: chartData, options: { @@ -308,7 +347,7 @@ function loadGraph(animationDuration = 1000) { enabled: true }, legend: { - display: true, + display: false, labels: { color: fontColor, // filter: function(item,chart) { diff --git a/packages/modules/web_themes/standard_legacy/web/style.css b/packages/modules/web_themes/standard_legacy/web/style.css index c522127e1b..1ba0cf9de3 100644 --- a/packages/modules/web_themes/standard_legacy/web/style.css +++ b/packages/modules/web_themes/standard_legacy/web/style.css @@ -476,3 +476,78 @@ h3 { transition: none; display: none; } + +/* Custom legend container */ +#custom-legend-container { + max-height: 120px; + margin-bottom: 10px; + overflow-y: auto; + border: 1px solid #ccc; + border-radius: 5px; + padding-top: 2px; + padding-bottom: 2px; + text-align: left; + display: flex; + flex-wrap: wrap; + justify-content: center; +} + +/* Legend item styling */ +.legend-item { + display: inline-flex; + align-items: center; + cursor: pointer; + user-select: none; + padding: 2px 5px; + border-radius: 3px; + font-size: 14px; + color: rgba(0,0,0,0.65); +} + +.legend-item:hover { + background-color: rgba(0,0,0,0.05); +} + +.legend-color-box { + display: inline-block; + width: 16px; + height: 4px; + margin-right: 4px; +} + +.legend-item.hidden { + opacity: 0.6; + text-decoration: line-through; +} + +/* Make the container responsive */ +@media (max-width: 768px) { + #custom-legend-container { + max-height: 100px; + } + + .legend-item { + font-size: 0.8em; + margin-right: 5px; + margin-bottom: 3px; + } + + .legend-color-box { + width: 12px; + height: 3px; + margin-right: 4px; + } +} + +/* For smaller screens */ +@media (max-width: 576px) { + #custom-legend-container { + max-height: 80px; + } + + .legend-color-box { + width: 10px; + height: 2px; + margin-right: 4px; + } +}