From 89b7a234c161f2c68b6a9499ff698488e1a35c6b Mon Sep 17 00:00:00 2001 From: =?utf8?q?J=C3=A9r=C3=B4me=20Benoit?= Date: Tue, 23 Aug 2022 13:07:43 +0200 Subject: [PATCH] UI server: add start/stop charging station command MIME-Version: 1.0 Content-Type: text/plain; charset=utf8 Content-Transfer-Encoding: 8bit + strong type broadcast channel protocol + UI protocol types definition refinement Signed-off-by: Jérôme Benoit --- src/charging-station/Bootstrap.ts | 12 ++-- src/charging-station/ChargingStation.ts | 52 ++------------ .../ChargingStationWorkerBroadcastChannel.ts | 67 +++++++++++++++++++ src/charging-station/MessageChannelUtils.ts | 1 + .../ui-services/AbstractUIService.ts | 14 ++-- .../ui-server/ui-services/UIService001.ts | 46 +++++++++++-- src/types/ChargingStationWorker.ts | 1 + src/types/UIProtocol.ts | 21 +++--- src/types/WorkerBroadcastChannel.ts | 21 +++++- 9 files changed, 160 insertions(+), 75 deletions(-) create mode 100644 src/charging-station/ChargingStationWorkerBroadcastChannel.ts diff --git a/src/charging-station/Bootstrap.ts b/src/charging-station/Bootstrap.ts index f881c19b..516730b9 100644 --- a/src/charging-station/Bootstrap.ts +++ b/src/charging-station/Bootstrap.ts @@ -38,6 +38,7 @@ export default class Bootstrap { private readonly storage!: Storage; private numberOfChargingStationTemplates!: number; private numberOfChargingStations!: number; + private numberOfStartedChargingStations!: number; private readonly version: string = version; private started: boolean; private readonly workerScript: string; @@ -219,12 +220,12 @@ export default class Bootstrap { private workerEventStarted(data: ChargingStationData) { this.uiServer?.chargingStations.set(data.hashId, data); - this.started && ++this.numberOfChargingStations; + ++this.numberOfStartedChargingStations; } private workerEventStopped(data: ChargingStationData) { - this.uiServer?.chargingStations.delete(data.hashId); - this.started && --this.numberOfChargingStations; + this.uiServer?.chargingStations.set(data.hashId, data); + --this.numberOfStartedChargingStations; } private workerEventUpdated(data: ChargingStationData) { @@ -236,8 +237,9 @@ export default class Bootstrap { }; private initialize() { - this.numberOfChargingStations = 0; this.numberOfChargingStationTemplates = 0; + this.numberOfChargingStations = 0; + this.numberOfStartedChargingStations = 0; this.initializeWorkerImplementation(); } @@ -255,7 +257,7 @@ export default class Bootstrap { ), }; await this.workerImplementation.addElement(workerData); - this.numberOfChargingStations++; + ++this.numberOfChargingStations; } private logPrefix(): string { diff --git a/src/charging-station/ChargingStation.ts b/src/charging-station/ChargingStation.ts index dd7cac5a..67cd9e9c 100644 --- a/src/charging-station/ChargingStation.ts +++ b/src/charging-station/ChargingStation.ts @@ -6,7 +6,7 @@ import path from 'path'; import { URL } from 'url'; import { parentPort } from 'worker_threads'; -import WebSocket, { Data, MessageEvent, RawData } from 'ws'; +import WebSocket, { Data, RawData } from 'ws'; import BaseError from '../exception/BaseError'; import OCPPError from '../exception/OCPPError'; @@ -58,15 +58,11 @@ import { StatusNotificationResponse, } from '../types/ocpp/Responses'; import { - StartTransactionRequest, - StartTransactionResponse, StopTransactionReason, StopTransactionRequest, StopTransactionResponse, } from '../types/ocpp/Transaction'; -import { ProcedureName } from '../types/UIProtocol'; import { WSError, WebSocketCloseEventStatusCode } from '../types/WebSocket'; -import { WorkerBroadcastChannelData } from '../types/WorkerBroadcastChannel'; import Configuration from '../utils/Configuration'; import Constants from '../utils/Constants'; import { ACElectricUtils, DCElectricUtils } from '../utils/ElectricUtils'; @@ -77,6 +73,7 @@ import AuthorizedTagsCache from './AuthorizedTagsCache'; import AutomaticTransactionGenerator from './AutomaticTransactionGenerator'; import { ChargingStationConfigurationUtils } from './ChargingStationConfigurationUtils'; import { ChargingStationUtils } from './ChargingStationUtils'; +import ChargingStationWorkerBroadcastChannel from './ChargingStationWorkerBroadcastChannel'; import { MessageChannelUtils } from './MessageChannelUtils'; import OCPP16IncomingRequestService from './ocpp/1.6/OCPP16IncomingRequestService'; import OCPP16RequestService from './ocpp/1.6/OCPP16RequestService'; @@ -85,13 +82,13 @@ import { OCPP16ServiceUtils } from './ocpp/1.6/OCPP16ServiceUtils'; import OCPPIncomingRequestService from './ocpp/OCPPIncomingRequestService'; import OCPPRequestService from './ocpp/OCPPRequestService'; import SharedLRUCache from './SharedLRUCache'; -import WorkerBroadcastChannel from './WorkerBroadcastChannel'; export default class ChargingStation { public hashId!: string; public readonly templateFile: string; public authorizedTagsCache: AuthorizedTagsCache; public stationInfo!: ChargingStationInfo; + public stopped: boolean; public readonly connectors: Map; public ocppConfiguration!: ChargingStationOcppConfiguration; public wsConnection!: WebSocket; @@ -111,12 +108,11 @@ export default class ChargingStation { private configuredSupervisionUrl!: URL; private wsConnectionRestarted: boolean; private autoReconnectRetryCount: number; - private stopped: boolean; private templateFileWatcher!: fs.FSWatcher; private readonly sharedLRUCache: SharedLRUCache; private automaticTransactionGenerator!: AutomaticTransactionGenerator; private webSocketPingSetInterval!: NodeJS.Timeout; - private workerBroadcastChannel: WorkerBroadcastChannel; + private readonly chargingStationWorkerBroadcastChannel: ChargingStationWorkerBroadcastChannel; constructor(index: number, templateFile: string) { this.index = index; @@ -129,11 +125,7 @@ export default class ChargingStation { this.connectors = new Map(); this.requests = new Map(); this.messageBuffer = new Set(); - this.workerBroadcastChannel = new WorkerBroadcastChannel(); - - this.workerBroadcastChannel.onmessage = this.handleWorkerBroadcastChannelMessage.bind(this) as ( - message: MessageEvent - ) => void; + this.chargingStationWorkerBroadcastChannel = new ChargingStationWorkerBroadcastChannel(this); this.initialize(); } @@ -2023,38 +2015,4 @@ export default class ChargingStation { this.getConnectorStatus(connectorId).energyActiveImportRegisterValue = 0; this.getConnectorStatus(connectorId).transactionEnergyActiveImportRegisterValue = 0; } - - private async handleWorkerBroadcastChannelMessage(message: MessageEvent): Promise { - const [command, payload] = message.data as unknown as [ - ProcedureName, - WorkerBroadcastChannelData - ]; - - if (payload.hashId !== this.hashId) { - return; - } - - switch (command) { - case ProcedureName.START_TRANSACTION: - await this.ocppRequestService.requestHandler< - StartTransactionRequest, - StartTransactionResponse - >(this, RequestCommand.START_TRANSACTION, { - connectorId: payload.connectorId, - idTag: payload.idTag, - }); - break; - case ProcedureName.STOP_TRANSACTION: - await this.ocppRequestService.requestHandler< - StopTransactionRequest, - StopTransactionResponse - >(this, RequestCommand.STOP_TRANSACTION, { - transactionId: payload.transactionId, - meterStop: this.getEnergyActiveImportRegisterByTransactionId(payload.transactionId), - idTag: this.getTransactionIdTag(payload.transactionId), - reason: StopTransactionReason.NONE, - }); - break; - } - } } diff --git a/src/charging-station/ChargingStationWorkerBroadcastChannel.ts b/src/charging-station/ChargingStationWorkerBroadcastChannel.ts new file mode 100644 index 00000000..5115d6cf --- /dev/null +++ b/src/charging-station/ChargingStationWorkerBroadcastChannel.ts @@ -0,0 +1,67 @@ +import { BroadcastChannel } from 'worker_threads'; + +import { RequestCommand } from '../types/ocpp/Requests'; +import { + StartTransactionRequest, + StartTransactionResponse, + StopTransactionReason, + StopTransactionRequest, + StopTransactionResponse, +} from '../types/ocpp/Transaction'; +import { + BroadcastChannelProcedureName, + BroadcastChannelRequest, +} from '../types/WorkerBroadcastChannel'; +import ChargingStation from './ChargingStation'; + +type MessageEvent = { data: unknown }; + +export default class ChargingStationWorkerBroadcastChannel extends BroadcastChannel { + private readonly chargingStation: ChargingStation; + + constructor(chargingStation: ChargingStation) { + super('worker'); + this.chargingStation = chargingStation; + this.onmessage = this.handleRequest.bind(this) as (message: MessageEvent) => void; + } + + private async handleRequest(messageEvent: MessageEvent): Promise { + const [, command, payload] = messageEvent.data as BroadcastChannelRequest; + + if (payload.hashId !== this.chargingStation.hashId) { + return; + } + + // TODO: return a response stating the command success or failure + switch (command) { + case BroadcastChannelProcedureName.START_TRANSACTION: + await this.chargingStation.ocppRequestService.requestHandler< + StartTransactionRequest, + StartTransactionResponse + >(this.chargingStation, RequestCommand.START_TRANSACTION, { + connectorId: payload.connectorId, + idTag: payload.idTag, + }); + break; + case BroadcastChannelProcedureName.STOP_TRANSACTION: + await this.chargingStation.ocppRequestService.requestHandler< + StopTransactionRequest, + StopTransactionResponse + >(this.chargingStation, RequestCommand.STOP_TRANSACTION, { + transactionId: payload.transactionId, + meterStop: this.chargingStation.getEnergyActiveImportRegisterByTransactionId( + payload.transactionId + ), + idTag: this.chargingStation.getTransactionIdTag(payload.transactionId), + reason: StopTransactionReason.NONE, + }); + break; + case BroadcastChannelProcedureName.START_CHARGING_STATION: + this.chargingStation.start(); + break; + case BroadcastChannelProcedureName.STOP_CHARGING_STATION: + await this.chargingStation.stop(); + break; + } + } +} diff --git a/src/charging-station/MessageChannelUtils.ts b/src/charging-station/MessageChannelUtils.ts index 6e37885b..28172461 100644 --- a/src/charging-station/MessageChannelUtils.ts +++ b/src/charging-station/MessageChannelUtils.ts @@ -53,6 +53,7 @@ export class MessageChannelUtils { return { hashId: chargingStation.hashId, stationInfo: chargingStation.stationInfo, + stopped: chargingStation.stopped, connectors: Array.from(chargingStation.connectors.values()), }; } diff --git a/src/charging-station/ui-server/ui-services/AbstractUIService.ts b/src/charging-station/ui-server/ui-services/AbstractUIService.ts index dcfd8244..070119bb 100644 --- a/src/charging-station/ui-server/ui-services/AbstractUIService.ts +++ b/src/charging-station/ui-server/ui-services/AbstractUIService.ts @@ -8,6 +8,7 @@ import { ProtocolRequestHandler, ProtocolResponse, ProtocolVersion, + RequestPayload, ResponsePayload, ResponseStatus, } from '../../../types/UIProtocol'; @@ -39,13 +40,12 @@ export default abstract class AbstractUIService { public async messageHandler(request: RawData): Promise { let messageId: string; let command: ProcedureName; - let requestPayload: JsonType; + let requestPayload: RequestPayload; let responsePayload: ResponsePayload; try { [messageId, command, requestPayload] = this.dataValidation(request); if (this.messageHandlers.has(command) === false) { - // Throw exception throw new BaseError( `${command} is not implemented to handle message payload ${JSON.stringify( requestPayload, @@ -54,10 +54,9 @@ export default abstract class AbstractUIService { )}` ); } + // Call the message handler to build the response payload - responsePayload = (await this.messageHandlers.get(command)( - requestPayload - )) as ResponsePayload; + responsePayload = await this.messageHandlers.get(command)(requestPayload); } catch (error) { // Log logger.error( @@ -107,11 +106,12 @@ export default abstract class AbstractUIService { return data as ProtocolRequest; } - private handleListChargingStations(): JsonType { + private handleListChargingStations(): ResponsePayload { + // TODO: remove cast to unknown return { status: ResponseStatus.SUCCESS, ...Array.from(this.uiServer.chargingStations.values()), - } as JsonType; + } as unknown as ResponsePayload; } private async handleStartSimulator(): Promise { diff --git a/src/charging-station/ui-server/ui-services/UIService001.ts b/src/charging-station/ui-server/ui-services/UIService001.ts index e322ef00..94060b5e 100644 --- a/src/charging-station/ui-server/ui-services/UIService001.ts +++ b/src/charging-station/ui-server/ui-services/UIService001.ts @@ -1,11 +1,13 @@ -import { JsonType } from '../../../types/JsonType'; import { ProcedureName, ProtocolRequestHandler, ProtocolVersion, + RequestPayload, ResponsePayload, ResponseStatus, } from '../../../types/UIProtocol'; +import { BroadcastChannelProcedureName } from '../../../types/WorkerBroadcastChannel'; +import Utils from '../../../utils/Utils'; import { AbstractUIServer } from '../AbstractUIServer'; import AbstractUIService from './AbstractUIService'; @@ -20,15 +22,49 @@ export default class UIService001 extends AbstractUIService { ProcedureName.STOP_TRANSACTION, this.handleStopTransaction.bind(this) as ProtocolRequestHandler ); + this.messageHandlers.set( + ProcedureName.START_CHARGING_STATION, + this.handleStartChargingStation.bind(this) as ProtocolRequestHandler + ); + this.messageHandlers.set( + ProcedureName.STOP_CHARGING_STATION, + this.handleStopChargingStation.bind(this) as ProtocolRequestHandler + ); + } + + private handleStartTransaction(payload: RequestPayload): ResponsePayload { + this.workerBroadcastChannel.postMessage([ + Utils.generateUUID(), + BroadcastChannelProcedureName.START_TRANSACTION, + payload, + ]); + return { status: ResponseStatus.SUCCESS }; + } + + private handleStopTransaction(payload: RequestPayload): ResponsePayload { + this.workerBroadcastChannel.postMessage([ + Utils.generateUUID(), + BroadcastChannelProcedureName.STOP_TRANSACTION, + payload, + ]); + return { status: ResponseStatus.SUCCESS }; } - private handleStartTransaction(payload: JsonType): ResponsePayload { - this.workerBroadcastChannel.postMessage([ProcedureName.START_TRANSACTION, payload]); + private handleStartChargingStation(payload: RequestPayload): ResponsePayload { + this.workerBroadcastChannel.postMessage([ + Utils.generateUUID(), + BroadcastChannelProcedureName.START_CHARGING_STATION, + payload, + ]); return { status: ResponseStatus.SUCCESS }; } - private handleStopTransaction(payload: JsonType): ResponsePayload { - this.workerBroadcastChannel.postMessage([ProcedureName.STOP_TRANSACTION, payload]); + private handleStopChargingStation(payload: RequestPayload): ResponsePayload { + this.workerBroadcastChannel.postMessage([ + Utils.generateUUID(), + BroadcastChannelProcedureName.STOP_CHARGING_STATION, + payload, + ]); return { status: ResponseStatus.SUCCESS }; } } diff --git a/src/types/ChargingStationWorker.ts b/src/types/ChargingStationWorker.ts index 0caf1a81..1a48e96d 100644 --- a/src/types/ChargingStationWorker.ts +++ b/src/types/ChargingStationWorker.ts @@ -16,6 +16,7 @@ export interface ChargingStationWorkerData extends WorkerData { export interface ChargingStationData extends WorkerData { hashId: string; stationInfo: ChargingStationInfo; + stopped: boolean; connectors: ConnectorStatus[]; } diff --git a/src/types/UIProtocol.ts b/src/types/UIProtocol.ts index f7a4ac0c..4c25c58e 100644 --- a/src/types/UIProtocol.ts +++ b/src/types/UIProtocol.ts @@ -1,4 +1,4 @@ -import { JsonObject, JsonType } from './JsonType'; +import { JsonObject } from './JsonType'; export enum Protocol { UI = 'ui', @@ -13,13 +13,25 @@ export enum ProtocolVersion { '0.0.1' = '0.0.1', } +export type ProtocolRequest = [string, ProcedureName, RequestPayload]; +export type ProtocolResponse = [string, ResponsePayload]; + +export type ProtocolRequestHandler = ( + payload: RequestPayload +) => ResponsePayload | Promise; + export enum ProcedureName { LIST_CHARGING_STATIONS = 'listChargingStations', + START_CHARGING_STATION = 'startChargingStation', + STOP_CHARGING_STATION = 'stopChargingStation', START_TRANSACTION = 'startTransaction', STOP_TRANSACTION = 'stopTransaction', START_SIMULATOR = 'startSimulator', STOP_SIMULATOR = 'stopSimulator', } +export interface RequestPayload extends JsonObject { + hashId?: string; +} export enum ResponseStatus { SUCCESS = 'success', @@ -29,10 +41,3 @@ export enum ResponseStatus { export interface ResponsePayload extends JsonObject { status: ResponseStatus; } - -export type ProtocolRequest = [string, ProcedureName, JsonType]; -export type ProtocolResponse = [string, ResponsePayload]; - -export type ProtocolRequestHandler = ( - payload: JsonType -) => void | Promise | ResponsePayload | Promise; diff --git a/src/types/WorkerBroadcastChannel.ts b/src/types/WorkerBroadcastChannel.ts index 61a47fa0..381f57e4 100644 --- a/src/types/WorkerBroadcastChannel.ts +++ b/src/types/WorkerBroadcastChannel.ts @@ -1,8 +1,23 @@ -import { ChargingStationData } from './ChargingStationWorker'; +import { JsonObject } from './JsonType'; -// TODO: use a base payload type and extends it per procedure name -export interface WorkerBroadcastChannelData extends ChargingStationData { +export type BroadcastChannelRequest = [string, BroadcastChannelProcedureName, RequestPayload]; +export type BroadcastChannelResponse = [string, ResponsePayload]; + +export enum BroadcastChannelProcedureName { + START_CHARGING_STATION = 'startChargingStation', + STOP_CHARGING_STATION = 'stopChargingStation', + START_TRANSACTION = 'startTransaction', + STOP_TRANSACTION = 'stopTransaction', +} + +interface BasePayload extends JsonObject { + hashId: string; +} + +export interface RequestPayload extends BasePayload { connectorId?: number; transactionId?: number; idTag?: string; } + +export type ResponsePayload = BasePayload; -- 2.34.1