From c5ecc04daa3a6102cd586af446547dee585d506b Mon Sep 17 00:00:00 2001 From: =?utf8?q?J=C3=A9r=C3=B4me=20Benoit?= Date: Thu, 1 Feb 2024 10:53:08 +0100 Subject: [PATCH] feat: add addChargingStations command to UI API MIME-Version: 1.0 Content-Type: text/plain; charset=utf8 Content-Transfer-Encoding: 8bit closes #964 Signed-off-by: Jérôme Benoit --- src/charging-station/Bootstrap.ts | 86 +++++++++++-------- .../ui-services/AbstractUIService.ts | 37 +++++++- src/types/UIProtocol.ts | 1 + 3 files changed, 88 insertions(+), 36 deletions(-) diff --git a/src/charging-station/Bootstrap.ts b/src/charging-station/Bootstrap.ts index ab935c7b..de438978 100644 --- a/src/charging-station/Bootstrap.ts +++ b/src/charging-station/Bootstrap.ts @@ -4,6 +4,7 @@ import { EventEmitter } from 'node:events' import { dirname, extname, join, parse } from 'node:path' import process, { exit } from 'node:process' import { fileURLToPath } from 'node:url' +import { isMainThread } from 'node:worker_threads' import type { Worker } from 'worker_threads' import chalk from 'chalk' @@ -23,7 +24,6 @@ import { ChargingStationWorkerMessageEvents, ConfigurationSection, ProcedureName, - type StationTemplateUrl, type Statistics, type StorageConfiguration, type UIServerConfiguration, @@ -39,7 +39,8 @@ import { isAsyncFunction, isNotEmptyArray, logPrefix, - logger + logger, + max } from '../utils/index.js' import { type WorkerAbstract, WorkerFactory } from '../worker/index.js' @@ -58,7 +59,11 @@ export class Bootstrap extends EventEmitter { private workerImplementation?: WorkerAbstract private readonly uiServer?: AbstractUIServer private storage?: Storage - private readonly chargingStationsByTemplate: Map + private readonly chargingStationsByTemplate: Map< + string, + { configured: number, started: number, lastIndex: number } + > + private readonly version: string = version private initializedCounters: boolean private started: boolean @@ -78,10 +83,7 @@ export class Bootstrap extends EventEmitter { this.stopping = false this.chargingStationsByTemplate = new Map< string, - { - configured: number - started: number - } + { configured: number, started: number, lastIndex: number } >() this.uiServer = UIServerFactory.getUIServerImplementation( Configuration.getConfigurationSection(ConfigurationSection.uiServer) @@ -89,7 +91,9 @@ export class Bootstrap extends EventEmitter { this.initializedCounters = false this.initializeCounters() Configuration.configurationChangeCallback = async () => { - await Bootstrap.getInstance().restart(false) + if (isMainThread) { + await Bootstrap.getInstance().restart() + } } } @@ -111,6 +115,10 @@ export class Bootstrap extends EventEmitter { ) } + public getLastIndex (templateName: string): number { + return this.chargingStationsByTemplate.get(templateName)?.lastIndex ?? 0 + } + private get numberOfStartedChargingStations (): number { return [...this.chargingStationsByTemplate.values()].reduce( (accumulator, value) => accumulator + value.started, @@ -159,7 +167,7 @@ export class Bootstrap extends EventEmitter { this.chargingStationsByTemplate.get(parse(stationTemplateUrl.file).name) ?.configured ?? stationTemplateUrl.numberOfStations for (let index = 1; index <= nbStations; index++) { - await this.startChargingStation(index, stationTemplateUrl) + await this.addChargingStation(index, stationTemplateUrl.file) } } catch (error) { console.error( @@ -174,7 +182,7 @@ export class Bootstrap extends EventEmitter { chalk.green( `Charging stations simulator ${ this.version - } started with ${this.numberOfConfiguredChargingStations} charging station(s) from ${this.numberOfChargingStationTemplates} configured charging station template(s) and ${ + } started with ${this.numberOfConfiguredChargingStations} configured charging station(s) from ${this.numberOfChargingStationTemplates} charging station template(s) and ${ Configuration.workerDynamicPoolInUse() ? `${workerConfiguration.poolMinSize}/` : '' }${this.workerImplementation?.size}${ Configuration.workerPoolInUse() ? `/${workerConfiguration.poolMaxSize}` : '' @@ -202,23 +210,21 @@ export class Bootstrap extends EventEmitter { } } - public async stop (stopChargingStations = true): Promise { + public async stop (): Promise { if (this.started) { if (!this.stopping) { this.stopping = true - if (stopChargingStations) { - await this.uiServer?.sendInternalRequest( - this.uiServer.buildProtocolRequest( - generateUUID(), - ProcedureName.STOP_CHARGING_STATION, - Constants.EMPTY_FROZEN_OBJECT - ) + await this.uiServer?.sendInternalRequest( + this.uiServer.buildProtocolRequest( + generateUUID(), + ProcedureName.STOP_CHARGING_STATION, + Constants.EMPTY_FROZEN_OBJECT ) - try { - await this.waitChargingStationsStopped() - } catch (error) { - console.error(chalk.red('Error while waiting for charging stations to stop: '), error) - } + ) + try { + await this.waitChargingStationsStopped() + } catch (error) { + console.error(chalk.red('Error while waiting for charging stations to stop: '), error) } await this.workerImplementation?.stop() delete this.workerImplementation @@ -235,8 +241,8 @@ export class Bootstrap extends EventEmitter { } } - public async restart (stopChargingStations?: boolean): Promise { - await this.stop(stopChargingStations) + private async restart (): Promise { + await this.stop() Configuration.getConfigurationSection(ConfigurationSection.uiServer) .enabled === false && this.uiServer?.stop() this.initializedCounters = false @@ -268,6 +274,9 @@ export class Bootstrap extends EventEmitter { } private initializeWorkerImplementation (workerConfiguration: WorkerConfiguration): void { + if (!isMainThread) { + return + } let elementsPerWorker: number | undefined switch (workerConfiguration.elementsPerWorker) { case 'auto': @@ -365,7 +374,7 @@ export class Bootstrap extends EventEmitter { data.stationInfo.chargingStationId } (hashId: ${data.stationInfo.hashId}) started (${ this.numberOfStartedChargingStations - } started from ${this.numberOfConfiguredChargingStations})` + } started from ${this.numberOfConfiguredChargingStations} configured charging station(s))` ) } @@ -378,7 +387,7 @@ export class Bootstrap extends EventEmitter { data.stationInfo.chargingStationId } (hashId: ${data.stationInfo.hashId}) stopped (${ this.numberOfStartedChargingStations - } started from ${this.numberOfConfiguredChargingStations})` + } started from ${this.numberOfConfiguredChargingStations} configured charging station(s))` ) } @@ -410,7 +419,8 @@ export class Bootstrap extends EventEmitter { const templateName = parse(stationTemplateUrl.file).name this.chargingStationsByTemplate.set(templateName, { configured: stationTemplateUrl.numberOfStations, - started: 0 + started: 0, + lastIndex: 0 }) this.uiServer?.chargingStationTemplates.add(templateName) } @@ -428,10 +438,14 @@ export class Bootstrap extends EventEmitter { ) exit(exitCodes.missingChargingStationsConfiguration) } - if (this.numberOfConfiguredChargingStations === 0) { + if ( + this.numberOfConfiguredChargingStations === 0 && + Configuration.getConfigurationSection(ConfigurationSection.uiServer) + .enabled === true + ) { console.error( chalk.red( - "'stationTemplateUrls' has no charging station enabled, please check your configuration" + "'stationTemplateUrls' has no charging station enabled and UI server is disabled, please check your configuration" ) ) exit(exitCodes.noChargingStationTemplates) @@ -440,19 +454,21 @@ export class Bootstrap extends EventEmitter { } } - private async startChargingStation ( - index: number, - stationTemplateUrl: StationTemplateUrl - ): Promise { + public async addChargingStation (index: number, stationTemplateFile: string): Promise { await this.workerImplementation?.addElement({ index, templateFile: join( dirname(fileURLToPath(import.meta.url)), 'assets', 'station-templates', - stationTemplateUrl.file + stationTemplateFile ) }) + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion + this.chargingStationsByTemplate.get(parse(stationTemplateFile).name)!.lastIndex = max( + index, + this.chargingStationsByTemplate.get(parse(stationTemplateFile).name)?.lastIndex ?? -Infinity + ) } private gracefulShutdown (): void { diff --git a/src/charging-station/ui-server/ui-services/AbstractUIService.ts b/src/charging-station/ui-server/ui-services/AbstractUIService.ts index 6eda2c9a..9d044a09 100644 --- a/src/charging-station/ui-server/ui-services/AbstractUIService.ts +++ b/src/charging-station/ui-server/ui-services/AbstractUIService.ts @@ -67,6 +67,7 @@ export abstract class AbstractUIService { this.requestHandlers = new Map([ [ProcedureName.LIST_TEMPLATES, this.handleListTemplates.bind(this)], [ProcedureName.LIST_CHARGING_STATIONS, this.handleListChargingStations.bind(this)], + [ProcedureName.ADD_CHARGING_STATIONS, this.handleAddChargingStations.bind(this)], [ProcedureName.START_SIMULATOR, this.handleStartSimulator.bind(this)], [ProcedureName.STOP_SIMULATOR, this.handleStopSimulator.bind(this)] ]) @@ -118,7 +119,7 @@ export abstract class AbstractUIService { errorMessage: (error as OCPPError).message, errorStack: (error as OCPPError).stack, errorDetails: (error as OCPPError).details - } + } satisfies ResponsePayload } if (responsePayload != null) { // eslint-disable-next-line @typescript-eslint/no-non-null-assertion @@ -217,6 +218,40 @@ export abstract class AbstractUIService { } satisfies ResponsePayload } + private async handleAddChargingStations ( + messageId?: string, + procedureName?: ProcedureName, + requestPayload?: RequestPayload + ): Promise { + const { template, numberOfStations } = requestPayload as { + template: string + numberOfStations: number + } + if (!this.uiServer.chargingStationTemplates.has(template)) { + return { + status: ResponseStatus.FAILURE, + errorMessage: `Template '${template}' not found` + } satisfies ResponsePayload + } + for (let i = 0; i < numberOfStations; i++) { + try { + await Bootstrap.getInstance().addChargingStation( + Bootstrap.getInstance().getLastIndex(template) + 1, + `${template}.json` + ) + } catch (error) { + return { + status: ResponseStatus.FAILURE, + errorMessage: (error as Error).message, + errorStack: (error as Error).stack + } satisfies ResponsePayload + } + } + return { + status: ResponseStatus.SUCCESS + } + } + private async handleStartSimulator (): Promise { try { await Bootstrap.getInstance().start() diff --git a/src/types/UIProtocol.ts b/src/types/UIProtocol.ts index 13da330e..265f3760 100644 --- a/src/types/UIProtocol.ts +++ b/src/types/UIProtocol.ts @@ -32,6 +32,7 @@ export enum ProcedureName { STOP_SIMULATOR = 'stopSimulator', LIST_TEMPLATES = 'listTemplates', LIST_CHARGING_STATIONS = 'listChargingStations', + ADD_CHARGING_STATIONS = 'addChargingStations', START_CHARGING_STATION = 'startChargingStation', STOP_CHARGING_STATION = 'stopChargingStation', OPEN_CONNECTION = 'openConnection', -- 2.34.1