feat: expose template stats to UI server simulatorState command
authorJérôme Benoit <jerome.benoit@sap.com>
Sat, 2 Mar 2024 13:12:28 +0000 (14:12 +0100)
committerJérôme Benoit <jerome.benoit@sap.com>
Sat, 2 Mar 2024 13:12:28 +0000 (14:12 +0100)
Signed-off-by: Jérôme Benoit <jerome.benoit@sap.com>
src/charging-station/Bootstrap.ts
src/types/Statistics.ts
src/types/UIProtocol.ts
src/types/index.ts
src/utils/MessageChannelUtils.ts
src/utils/index.ts
ui/web/src/types/UIProtocol.ts
ui/web/src/types/index.ts
ui/web/src/views/ChargingStationsView.vue

index 9247ab3deb5112925a4a7f8f215c31a5823712ff..f1f214b53296c1c812cfe31da93875a1742bdbe2 100644 (file)
@@ -25,7 +25,9 @@ import {
   type ChargingStationWorkerMessageData,
   ChargingStationWorkerMessageEvents,
   ConfigurationSection,
+  type InternalTemplateStatistics,
   ProcedureName,
+  type SimulatorState,
   type Statistics,
   type StorageConfiguration,
   type UIServerConfiguration,
@@ -34,6 +36,7 @@ import {
 import {
   Configuration,
   Constants,
+  buildTemplateStatisticsPayload,
   formatDurationMilliSeconds,
   generateUUID,
   handleUncaughtException,
@@ -55,19 +58,12 @@ enum exitCodes {
   gracefulShutdownError = 4
 }
 
-interface TemplateChargingStations {
-  configured: number
-  added: number
-  started: number
-  indexes: Set<number>
-}
-
 export class Bootstrap extends EventEmitter {
   private static instance: Bootstrap | null = null
   private workerImplementation?: WorkerAbstract<ChargingStationWorkerData>
   private readonly uiServer: AbstractUIServer
   private storage?: Storage
-  private readonly templatesChargingStations: Map<string, TemplateChargingStations>
+  private readonly templateStatistics: Map<string, InternalTemplateStatistics>
   private readonly version: string = version
   private initializedCounters: boolean
   private started: boolean
@@ -90,7 +86,7 @@ export class Bootstrap extends EventEmitter {
     this.uiServer = UIServerFactory.getUIServerImplementation(
       Configuration.getConfigurationSection<UIServerConfiguration>(ConfigurationSection.uiServer)
     )
-    this.templatesChargingStations = new Map<string, TemplateChargingStations>()
+    this.templateStatistics = new Map<string, InternalTemplateStatistics>()
     this.initializedCounters = false
     this.initializeCounters()
     Configuration.configurationChangeCallback = async () => {
@@ -108,25 +104,27 @@ export class Bootstrap extends EventEmitter {
   }
 
   public get numberOfChargingStationTemplates (): number {
-    return this.templatesChargingStations.size
+    return this.templateStatistics.size
   }
 
   public get numberOfConfiguredChargingStations (): number {
-    return [...this.templatesChargingStations.values()].reduce(
+    return [...this.templateStatistics.values()].reduce(
       (accumulator, value) => accumulator + value.configured,
       0
     )
   }
 
-  public getState (): { started: boolean } {
+  public getState (): SimulatorState {
     return {
-      started: this.started
+      version: this.version,
+      started: this.started,
+      templateStatistics: buildTemplateStatisticsPayload(this.templateStatistics)
     }
   }
 
   public getLastIndex (templateName: string): number {
     // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
-    const indexes = [...this.templatesChargingStations.get(templateName)!.indexes]
+    const indexes = [...this.templateStatistics.get(templateName)!.indexes]
       .concat(0)
       .sort((a, b) => a - b)
     for (let i = 0; i < indexes.length - 1; i++) {
@@ -142,14 +140,14 @@ export class Bootstrap extends EventEmitter {
   }
 
   private get numberOfAddedChargingStations (): number {
-    return [...this.templatesChargingStations.values()].reduce(
+    return [...this.templateStatistics.values()].reduce(
       (accumulator, value) => accumulator + value.added,
       0
     )
   }
 
   private get numberOfStartedChargingStations (): number {
-    return [...this.templatesChargingStations.values()].reduce(
+    return [...this.templateStatistics.values()].reduce(
       (accumulator, value) => accumulator + value.started,
       0
     )
@@ -211,7 +209,7 @@ export class Bootstrap extends EventEmitter {
         for (const stationTemplateUrl of Configuration.getStationTemplateUrls()!) {
           try {
             const nbStations =
-              this.templatesChargingStations.get(parse(stationTemplateUrl.file).name)?.configured ??
+              this.templateStatistics.get(parse(stationTemplateUrl.file).name)?.configured ??
               stationTemplateUrl.numberOfStations
             for (let index = 1; index <= nbStations; index++) {
               await this.addChargingStation(index, stationTemplateUrl.file)
@@ -434,11 +432,9 @@ export class Bootstrap extends EventEmitter {
   private readonly workerEventDeleted = (data: ChargingStationData): void => {
     this.uiServer.chargingStations.delete(data.stationInfo.hashId)
     // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
-    const templateChargingStations = this.templatesChargingStations.get(
-      data.stationInfo.templateName
-    )!
-    --templateChargingStations.added
-    templateChargingStations.indexes.delete(data.stationInfo.templateIndex)
+    const templateStatistics = this.templateStatistics.get(data.stationInfo.templateName)!
+    --templateStatistics.added
+    templateStatistics.indexes.delete(data.stationInfo.templateIndex)
     logger.info(
       `${this.logPrefix()} ${moduleName}.workerEventDeleted: Charging station ${
         data.stationInfo.chargingStationId
@@ -451,7 +447,7 @@ export class Bootstrap extends EventEmitter {
   private readonly workerEventStarted = (data: ChargingStationData): void => {
     this.uiServer.chargingStations.set(data.stationInfo.hashId, data)
     // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
-    ++this.templatesChargingStations.get(data.stationInfo.templateName)!.started
+    ++this.templateStatistics.get(data.stationInfo.templateName)!.started
     logger.info(
       `${this.logPrefix()} ${moduleName}.workerEventStarted: Charging station ${
         data.stationInfo.chargingStationId
@@ -464,7 +460,7 @@ export class Bootstrap extends EventEmitter {
   private readonly workerEventStopped = (data: ChargingStationData): void => {
     this.uiServer.chargingStations.set(data.stationInfo.hashId, data)
     // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
-    --this.templatesChargingStations.get(data.stationInfo.templateName)!.started
+    --this.templateStatistics.get(data.stationInfo.templateName)!.started
     logger.info(
       `${this.logPrefix()} ${moduleName}.workerEventStopped: Charging station ${
         data.stationInfo.chargingStationId
@@ -500,7 +496,7 @@ export class Bootstrap extends EventEmitter {
       if (isNotEmptyArray(stationTemplateUrls)) {
         for (const stationTemplateUrl of stationTemplateUrls) {
           const templateName = parse(stationTemplateUrl.file).name
-          this.templatesChargingStations.set(templateName, {
+          this.templateStatistics.set(templateName, {
             configured: stationTemplateUrl.numberOfStations,
             added: 0,
             started: 0,
@@ -508,7 +504,7 @@ export class Bootstrap extends EventEmitter {
           })
           this.uiServer.chargingStationTemplates.add(templateName)
         }
-        if (this.templatesChargingStations.size !== stationTemplateUrls.length) {
+        if (this.templateStatistics.size !== stationTemplateUrls.length) {
           console.error(
             chalk.red(
               "'stationTemplateUrls' contains duplicate entries, please check your configuration"
@@ -554,11 +550,9 @@ export class Bootstrap extends EventEmitter {
       options
     })
     // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
-    const templateChargingStations = this.templatesChargingStations.get(
-      parse(stationTemplateFile).name
-    )!
-    ++templateChargingStations.added
-    templateChargingStations.indexes.add(index)
+    const templateStatistics = this.templateStatistics.get(parse(stationTemplateFile).name)!
+    ++templateStatistics.added
+    templateStatistics.indexes.add(index)
   }
 
   private gracefulShutdown (): void {
index 9bb614ee2654509daec9b1a90551a5e52650b9bd..0c7375d9c0ade1d0dcd8b19b856f6f439479af47 100644 (file)
@@ -31,3 +31,10 @@ export type Statistics = {
   updatedAt?: Date
   statisticsData: Map<string | RequestCommand | IncomingRequestCommand, StatisticsData>
 } & WorkerData
+
+export interface InternalTemplateStatistics {
+  configured: number
+  added: number
+  started: number
+  indexes: Set<number>
+}
index 37ec8ba924415fff78683ceebed9d2f62b2eab1f..b1b71b73fda1654c6bda98c67af43a32b85741c5 100644 (file)
@@ -72,3 +72,16 @@ export interface ResponsePayload extends JsonObject {
   hashIdsFailed?: string[]
   responsesFailed?: BroadcastChannelResponsePayload[]
 }
+
+export interface TemplateStatistics extends JsonObject {
+  configured: number
+  added: number
+  started: number
+  indexes: number[]
+}
+
+export interface SimulatorState extends JsonObject {
+  version: string
+  started: boolean
+  templateStatistics: Record<string, TemplateStatistics>
+}
index d5f887f81c3014c4a874063969608f433965166a..f312480a0eed1e4cda5865df7ebb97d2c560da70 100644 (file)
@@ -9,7 +9,9 @@ export {
   ProtocolVersion,
   type RequestPayload,
   type ResponsePayload,
-  ResponseStatus
+  ResponseStatus,
+  type SimulatorState,
+  type TemplateStatistics
 } from './UIProtocol.js'
 export {
   type AutomaticTransactionGeneratorConfiguration,
@@ -263,7 +265,12 @@ export type {
 export { OCPP20OptionalVariableName } from './ocpp/2.0/Variables.js'
 export { OCPPVersion } from './ocpp/OCPPVersion.js'
 export { PerformanceRecord } from './orm/entities/PerformanceRecord.js'
-export type { Statistics, StatisticsData, TimestampedData } from './Statistics.js'
+export type {
+  InternalTemplateStatistics,
+  Statistics,
+  StatisticsData,
+  TimestampedData
+} from './Statistics.js'
 export {
   type WSError,
   WebSocketCloseEventStatusCode,
index 36f797f5eb88feb3a9f5e0a4b6ea978b660a9f9b..65dcc9a835c36fa4fcf1cb6307e0d1063957fb6f 100644 (file)
@@ -4,12 +4,15 @@ import {
   buildConnectorsStatus,
   buildEvsesStatus
 } from './ChargingStationConfigurationUtils.js'
+import { clone } from './Utils.js'
 import type { ChargingStation } from '../charging-station/index.js'
 import {
   type ChargingStationData,
   type ChargingStationWorkerMessage,
   ChargingStationWorkerMessageEvents,
-  type Statistics
+  type InternalTemplateStatistics,
+  type Statistics,
+  type TemplateStatistics
 } from '../types/index.js'
 
 export const buildAddedMessage = (
@@ -86,3 +89,13 @@ export const buildChargingStationDataPayload = (
     })
   }
 }
+
+export const buildTemplateStatisticsPayload = (
+  map: Map<string, InternalTemplateStatistics>
+): Record<string, TemplateStatistics> => {
+  map = clone(map)
+  for (const value of map.values()) {
+    (value as unknown as TemplateStatistics).indexes = [...value.indexes]
+  }
+  return Object.fromEntries(map.entries() as unknown as Array<[string, TemplateStatistics]>)
+}
index 21a650ef42d92d1d69a6b67bc1b85cfe083ce811..1bc4fe7e0d64d5b4a161b476194d56d863eaa3b6 100644 (file)
@@ -24,6 +24,7 @@ export {
   buildPerformanceStatisticsMessage,
   buildStartedMessage,
   buildStoppedMessage,
+  buildTemplateStatisticsPayload,
   buildUpdatedMessage
 } from './MessageChannelUtils.js'
 export {
index 183098955bccf63f8b203aed569c5180bd883aea..5b3efcdcef1efb81cccc8eeddc5525ccda0797f1 100644 (file)
@@ -57,3 +57,16 @@ export interface ResponsePayload extends JsonObject {
   status: ResponseStatus
   hashIds?: string[]
 }
+
+interface TemplateStatistics extends JsonObject {
+  configured: number
+  added: number
+  started: number
+  indexes: number[]
+}
+
+export interface SimulatorState extends JsonObject {
+  version: string
+  started: boolean
+  templateStatistics: Record<string, TemplateStatistics>
+}
index a201ca7b88e0ee92e48635abf8ef63285a0fe136..d730f0d634160a7642660d90251e0c4fdae6d198 100644 (file)
@@ -15,5 +15,6 @@ export {
   ProtocolVersion,
   type RequestPayload,
   type ResponsePayload,
-  ResponseStatus
+  ResponseStatus,
+  type SimulatorState
 } from './UIProtocol'
index b15ce3f5ff338645abebcda0681244d9a28a8bd2..2b9695e43702801a36428e7235cab72eb772f5de 100644 (file)
@@ -72,6 +72,7 @@
         "
       >
         {{ state.simulatorState?.started === true ? 'Stop' : 'Start' }} Simulator
+        {{ state.simulatorState?.version != null ? ` (${state.simulatorState?.version})` : '' }}
       </ToggleButton>
       <ToggleButton
         :id="'add-charging-stations'"
 import { getCurrentInstance, onMounted, ref } from 'vue'
 import { useToast } from 'vue-toast-notification'
 import CSTable from '@/components/charging-stations/CSTable.vue'
-import type { ResponsePayload, UIServerConfigurationSection } from '@/types'
+import type { ResponsePayload, SimulatorState, UIServerConfigurationSection } from '@/types'
 import Container from '@/components/Container.vue'
 import ReloadButton from '@/components/buttons/ReloadButton.vue'
 import {
@@ -139,7 +140,7 @@ const state = ref<{
   renderAddChargingStations: `${string}-${string}-${string}-${string}-${string}`
   renderChargingStations: `${string}-${string}-${string}-${string}-${string}`
   loading: boolean
-  simulatorState?: { started: boolean }
+  simulatorState?: SimulatorState
   uiServerIndex: number
 }>({
   renderSimulator: randomUUID(),
@@ -170,7 +171,7 @@ const getSimulatorState = (): void => {
   uiClient
     .simulatorState()
     .then((response: ResponsePayload) => {
-      state.value.simulatorState = response.state as { started: boolean }
+      state.value.simulatorState = response.state as SimulatorState
     })
     .catch((error: Error) => {
       $toast.error('Error at fetching simulator state')