UI server: add start/stop charging station command
authorJérôme Benoit <jerome.benoit@sap.com>
Tue, 23 Aug 2022 11:07:43 +0000 (13:07 +0200)
committerJérôme Benoit <jerome.benoit@sap.com>
Tue, 23 Aug 2022 11:07:43 +0000 (13:07 +0200)
+ strong type broadcast channel protocol
+ UI protocol types definition refinement

Signed-off-by: Jérôme Benoit <jerome.benoit@sap.com>
src/charging-station/Bootstrap.ts
src/charging-station/ChargingStation.ts
src/charging-station/ChargingStationWorkerBroadcastChannel.ts [new file with mode: 0644]
src/charging-station/MessageChannelUtils.ts
src/charging-station/ui-server/ui-services/AbstractUIService.ts
src/charging-station/ui-server/ui-services/UIService001.ts
src/types/ChargingStationWorker.ts
src/types/UIProtocol.ts
src/types/WorkerBroadcastChannel.ts

index f881c19bfb0c77436dddf8aa39e37bfd3f954fbd..516730b9ec87da6d25e0561cd39bbb7b2c763832 100644 (file)
@@ -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 {
index dd7cac5a4e51446d543affaa513db6fe906b140b..67cd9e9ceed581df5feb33a3374617a091d7198b 100644 (file)
@@ -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<number, ConnectorStatus>;
   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<number, ConnectorStatus>();
     this.requests = new Map<string, CachedRequest>();
     this.messageBuffer = new Set<string>();
-    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<void> {
-    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 (file)
index 0000000..5115d6c
--- /dev/null
@@ -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<void> {
+    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;
+    }
+  }
+}
index 6e37885b1f8d0e7e7a5ea14e3c3f2955fd220402..28172461a6895774d6f2c0651849bce039e6754c 100644 (file)
@@ -53,6 +53,7 @@ export class MessageChannelUtils {
     return {
       hashId: chargingStation.hashId,
       stationInfo: chargingStation.stationInfo,
+      stopped: chargingStation.stopped,
       connectors: Array.from(chargingStation.connectors.values()),
     };
   }
index dcfd82447de4f20f498e293016fde4d232f96fd4..070119bb2642f4c0658c7d4e163ac48ea8c0e7f6 100644 (file)
@@ -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<void> {
     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<ResponsePayload> {
index e322ef003dfc28dec225f3fa1a036b10e94563aa..94060b5e9b1efeac4fbbd63095fc28bfa444394c 100644 (file)
@@ -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 };
   }
 }
index 0caf1a81b082b491979cc5d8b16752fa32b14b07..1a48e96da8dc3557ce4be95d600a591ea00b8eb4 100644 (file)
@@ -16,6 +16,7 @@ export interface ChargingStationWorkerData extends WorkerData {
 export interface ChargingStationData extends WorkerData {
   hashId: string;
   stationInfo: ChargingStationInfo;
+  stopped: boolean;
   connectors: ConnectorStatus[];
 }
 
index f7a4ac0c9fa286b2908bbec3ae8c38ab1aa48113..4c25c58e921175b7f41535012591228d9feb24ff 100644 (file)
@@ -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<ResponsePayload>;
+
 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<void> | ResponsePayload | Promise<ResponsePayload>;
index 61a47fa02eb272e0b98ca41efdee3c465d3175c4..381f57e41b03e2f9d0bba89aeb3e09662b211729 100644 (file)
@@ -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;