Add initial classes structure for the OCPP 2.0 stack
authorJérôme Benoit <jerome.benoit@sap.com>
Wed, 4 Jan 2023 19:42:07 +0000 (20:42 +0100)
committerJérôme Benoit <jerome.benoit@sap.com>
Wed, 4 Jan 2023 19:42:07 +0000 (20:42 +0100)
Signed-off-by: Jérôme Benoit <jerome.benoit@sap.com>
src/charging-station/ocpp/1.6/OCPP16ServiceUtils.ts
src/charging-station/ocpp/2.0/OCPP20IncomingRequestService.ts [new file with mode: 0644]
src/charging-station/ocpp/2.0/OCPP20RequestService.ts [new file with mode: 0644]
src/charging-station/ocpp/2.0/OCPP20ResponseService.ts [new file with mode: 0644]
src/charging-station/ocpp/2.0/OCPP20ServiceUtils.ts [new file with mode: 0644]
src/types/Statistics.ts
src/types/ocpp/1.6/Requests.ts
src/types/ocpp/2.0/Requests.ts [new file with mode: 0644]
src/types/ocpp/2.0/Responses.ts [new file with mode: 0644]
src/types/ocpp/Requests.ts

index bb129482faef33099ebb30c4b812fd803a98cbdd..f3f22e45c8ff8f1456c9d9be4ebb3cc153e34495 100644 (file)
@@ -22,7 +22,7 @@ import {
   type OCPP16SampledValue,
 } from '../../../types/ocpp/1.6/MeterValues';
 import {
-  OCPP16IncomingRequestCommand,
+  type OCPP16IncomingRequestCommand,
   OCPP16RequestCommand,
 } from '../../../types/ocpp/1.6/Requests';
 import { ErrorType } from '../../../types/ocpp/ErrorType';
diff --git a/src/charging-station/ocpp/2.0/OCPP20IncomingRequestService.ts b/src/charging-station/ocpp/2.0/OCPP20IncomingRequestService.ts
new file mode 100644 (file)
index 0000000..076832a
--- /dev/null
@@ -0,0 +1,131 @@
+// Partial Copyright Jerome Benoit. 2021. All Rights Reserved.
+
+import type { JSONSchemaType } from 'ajv';
+
+import OCPPError from '../../../exception/OCPPError';
+import type { JsonObject, JsonType } from '../../../types/JsonType';
+import type { OCPP20IncomingRequestCommand } from '../../../types/ocpp/2.0/Requests';
+import { ErrorType } from '../../../types/ocpp/ErrorType';
+import type { IncomingRequestHandler } from '../../../types/ocpp/Requests';
+import logger from '../../../utils/Logger';
+import type ChargingStation from '../../ChargingStation';
+import OCPPIncomingRequestService from '../OCPPIncomingRequestService';
+import { OCPP20ServiceUtils } from './OCPP20ServiceUtils';
+
+const moduleName = 'OCPP20IncomingRequestService';
+
+export default class OCPP20IncomingRequestService extends OCPPIncomingRequestService {
+  private incomingRequestHandlers: Map<OCPP20IncomingRequestCommand, IncomingRequestHandler>;
+  private jsonSchemas: Map<OCPP20IncomingRequestCommand, JSONSchemaType<JsonObject>>;
+
+  public constructor() {
+    if (new.target?.name === moduleName) {
+      throw new TypeError(`Cannot construct ${new.target?.name} instances directly`);
+    }
+    super();
+    this.incomingRequestHandlers = new Map<OCPP20IncomingRequestCommand, IncomingRequestHandler>();
+    this.jsonSchemas = new Map<OCPP20IncomingRequestCommand, JSONSchemaType<JsonObject>>();
+    this.validatePayload.bind(this);
+  }
+
+  public async incomingRequestHandler(
+    chargingStation: ChargingStation,
+    messageId: string,
+    commandName: OCPP20IncomingRequestCommand,
+    commandPayload: JsonType
+  ): Promise<void> {
+    let response: JsonType;
+    if (
+      chargingStation.getOcppStrictCompliance() === true &&
+      chargingStation.isInPendingState() === true /* &&
+       (commandName === OCPP20IncomingRequestCommand.REMOTE_START_TRANSACTION ||
+        commandName === OCPP20IncomingRequestCommand.REMOTE_STOP_TRANSACTION ) */
+    ) {
+      throw new OCPPError(
+        ErrorType.SECURITY_ERROR,
+        `${commandName} cannot be issued to handle request PDU ${JSON.stringify(
+          commandPayload,
+          null,
+          2
+        )} while the charging station is in pending state on the central server`,
+        commandName,
+        commandPayload
+      );
+    }
+    if (
+      chargingStation.isRegistered() === true ||
+      (chargingStation.getOcppStrictCompliance() === false &&
+        chargingStation.isInUnknownState() === true)
+    ) {
+      if (
+        this.incomingRequestHandlers.has(commandName) === true &&
+        OCPP20ServiceUtils.isIncomingRequestCommandSupported(chargingStation, commandName) === true
+      ) {
+        try {
+          this.validatePayload(chargingStation, commandName, commandPayload);
+          // Call the method to build the response
+          response = await this.incomingRequestHandlers.get(commandName)(
+            chargingStation,
+            commandPayload
+          );
+        } catch (error) {
+          // Log
+          logger.error(
+            `${chargingStation.logPrefix()} ${moduleName}.incomingRequestHandler: Handle incoming request error:`,
+            error
+          );
+          throw error;
+        }
+      } else {
+        // Throw exception
+        throw new OCPPError(
+          ErrorType.NOT_IMPLEMENTED,
+          `${commandName} is not implemented to handle request PDU ${JSON.stringify(
+            commandPayload,
+            null,
+            2
+          )}`,
+          commandName,
+          commandPayload
+        );
+      }
+    } else {
+      throw new OCPPError(
+        ErrorType.SECURITY_ERROR,
+        `${commandName} cannot be issued to handle request PDU ${JSON.stringify(
+          commandPayload,
+          null,
+          2
+        )} while the charging station is not registered on the central server.`,
+        commandName,
+        commandPayload
+      );
+    }
+    // Send the built response
+    await chargingStation.ocppRequestService.sendResponse(
+      chargingStation,
+      messageId,
+      response,
+      commandName
+    );
+  }
+
+  private validatePayload(
+    chargingStation: ChargingStation,
+    commandName: OCPP20IncomingRequestCommand,
+    commandPayload: JsonType
+  ): boolean {
+    if (this.jsonSchemas.has(commandName)) {
+      return this.validateIncomingRequestPayload(
+        chargingStation,
+        commandName,
+        this.jsonSchemas.get(commandName),
+        commandPayload
+      );
+    }
+    logger.warn(
+      `${chargingStation.logPrefix()} ${moduleName}.validatePayload: No JSON schema found for command ${commandName} PDU validation`
+    );
+    return false;
+  }
+}
diff --git a/src/charging-station/ocpp/2.0/OCPP20RequestService.ts b/src/charging-station/ocpp/2.0/OCPP20RequestService.ts
new file mode 100644 (file)
index 0000000..8dfdce6
--- /dev/null
@@ -0,0 +1,99 @@
+// Partial Copyright Jerome Benoit. 2021. All Rights Reserved.
+
+import type { JSONSchemaType } from 'ajv';
+
+import OCPPError from '../../../exception/OCPPError';
+import type { JsonObject, JsonType } from '../../../types/JsonType';
+import type { OCPP20RequestCommand } from '../../../types/ocpp/2.0/Requests';
+import { ErrorType } from '../../../types/ocpp/ErrorType';
+import type { RequestParams } from '../../../types/ocpp/Requests';
+import logger from '../../../utils/Logger';
+import Utils from '../../../utils/Utils';
+import type ChargingStation from '../../ChargingStation';
+import OCPPRequestService from '../OCPPRequestService';
+import type OCPPResponseService from '../OCPPResponseService';
+import { OCPP20ServiceUtils } from './OCPP20ServiceUtils';
+
+const moduleName = 'OCPP20RequestService';
+
+export default class OCPP20RequestService extends OCPPRequestService {
+  private jsonSchemas: Map<OCPP20RequestCommand, JSONSchemaType<JsonObject>>;
+
+  public constructor(ocppResponseService: OCPPResponseService) {
+    if (new.target?.name === moduleName) {
+      throw new TypeError(`Cannot construct ${new.target?.name} instances directly`);
+    }
+    super(ocppResponseService);
+    this.jsonSchemas = new Map<OCPP20RequestCommand, JSONSchemaType<JsonObject>>();
+    this.buildRequestPayload.bind(this);
+    this.validatePayload.bind(this);
+  }
+
+  public async requestHandler<RequestType extends JsonType, ResponseType extends JsonType>(
+    chargingStation: ChargingStation,
+    commandName: OCPP20RequestCommand,
+    commandParams?: JsonType,
+    params?: RequestParams
+  ): Promise<ResponseType> {
+    if (OCPP20ServiceUtils.isRequestCommandSupported(chargingStation, commandName) === true) {
+      const requestPayload = this.buildRequestPayload<RequestType>(
+        chargingStation,
+        commandName,
+        commandParams
+      );
+      this.validatePayload(chargingStation, commandName, requestPayload);
+      return (await this.sendMessage(
+        chargingStation,
+        Utils.generateUUID(),
+        requestPayload,
+        commandName,
+        params
+      )) as unknown as ResponseType;
+    }
+    // OCPPError usage here is debatable: it's an error in the OCPP stack but not targeted to sendError().
+    throw new OCPPError(
+      ErrorType.NOT_SUPPORTED,
+      `Unsupported OCPP command '${commandName}'`,
+      commandName,
+      commandParams
+    );
+  }
+
+  private buildRequestPayload<Request extends JsonType>(
+    chargingStation: ChargingStation,
+    commandName: OCPP20RequestCommand,
+    commandParams?: JsonType
+  ): Request {
+    commandParams = commandParams as JsonObject;
+    switch (commandName) {
+      default:
+        // OCPPError usage here is debatable: it's an error in the OCPP stack but not targeted to sendError().
+        throw new OCPPError(
+          ErrorType.NOT_SUPPORTED,
+          // eslint-disable-next-line @typescript-eslint/restrict-template-expressions
+          `Unsupported OCPP command '${commandName}'`,
+          commandName,
+          commandParams
+        );
+    }
+  }
+
+  private validatePayload<Request extends JsonType>(
+    chargingStation: ChargingStation,
+    commandName: OCPP20RequestCommand,
+    requestPayload: Request
+  ): boolean {
+    if (this.jsonSchemas.has(commandName)) {
+      return this.validateRequestPayload(
+        chargingStation,
+        commandName,
+        this.jsonSchemas.get(commandName),
+        requestPayload
+      );
+    }
+    logger.warn(
+      `${chargingStation.logPrefix()} ${moduleName}.validatePayload: No JSON schema found for command ${commandName} PDU validation`
+    );
+    return false;
+  }
+}
diff --git a/src/charging-station/ocpp/2.0/OCPP20ResponseService.ts b/src/charging-station/ocpp/2.0/OCPP20ResponseService.ts
new file mode 100644 (file)
index 0000000..db320f6
--- /dev/null
@@ -0,0 +1,100 @@
+// Partial Copyright Jerome Benoit. 2021. All Rights Reserved.
+
+import type { JSONSchemaType } from 'ajv';
+
+import OCPPError from '../../../exception/OCPPError';
+import type { JsonObject, JsonType } from '../../../types/JsonType';
+import type { OCPP20RequestCommand } from '../../../types/ocpp/2.0/Requests';
+import { ErrorType } from '../../../types/ocpp/ErrorType';
+import type { ResponseHandler } from '../../../types/ocpp/Responses';
+import logger from '../../../utils/Logger';
+import type ChargingStation from '../../ChargingStation';
+import OCPPResponseService from '../OCPPResponseService';
+import { OCPP20ServiceUtils } from './OCPP20ServiceUtils';
+
+const moduleName = 'OCPP20ResponseService';
+
+export default class OCPP20ResponseService extends OCPPResponseService {
+  private responseHandlers: Map<OCPP20RequestCommand, ResponseHandler>;
+  private jsonSchemas: Map<OCPP20RequestCommand, JSONSchemaType<JsonObject>>;
+
+  public constructor() {
+    if (new.target?.name === moduleName) {
+      throw new TypeError(`Cannot construct ${new.target?.name} instances directly`);
+    }
+    super();
+    this.responseHandlers = new Map<OCPP20RequestCommand, ResponseHandler>();
+    this.jsonSchemas = new Map<OCPP20RequestCommand, JSONSchemaType<JsonObject>>();
+    this.validatePayload.bind(this);
+  }
+
+  public async responseHandler(
+    chargingStation: ChargingStation,
+    commandName: OCPP20RequestCommand,
+    payload: JsonType,
+    requestPayload: JsonType
+  ): Promise<void> {
+    if (
+      chargingStation.isRegistered() === true /* ||
+      commandName === OCPP20RequestCommand.BOOT_NOTIFICATION */
+    ) {
+      if (
+        this.responseHandlers.has(commandName) === true &&
+        OCPP20ServiceUtils.isRequestCommandSupported(chargingStation, commandName) === true
+      ) {
+        try {
+          this.validatePayload(chargingStation, commandName, payload);
+          await this.responseHandlers.get(commandName)(chargingStation, payload, requestPayload);
+        } catch (error) {
+          logger.error(
+            `${chargingStation.logPrefix()} ${moduleName}.responseHandler: Handle response error:`,
+            error
+          );
+          throw error;
+        }
+      } else {
+        // Throw exception
+        throw new OCPPError(
+          ErrorType.NOT_IMPLEMENTED,
+          `${commandName} is not implemented to handle response PDU ${JSON.stringify(
+            payload,
+            null,
+            2
+          )}`,
+          commandName,
+          payload
+        );
+      }
+    } else {
+      throw new OCPPError(
+        ErrorType.SECURITY_ERROR,
+        `${commandName} cannot be issued to handle response PDU ${JSON.stringify(
+          payload,
+          null,
+          2
+        )} while the charging station is not registered on the central server. `,
+        commandName,
+        payload
+      );
+    }
+  }
+
+  private validatePayload(
+    chargingStation: ChargingStation,
+    commandName: OCPP20RequestCommand,
+    payload: JsonType
+  ): boolean {
+    if (this.jsonSchemas.has(commandName)) {
+      return this.validateResponsePayload(
+        chargingStation,
+        commandName,
+        this.jsonSchemas.get(commandName),
+        payload
+      );
+    }
+    logger.warn(
+      `${chargingStation.logPrefix()} ${moduleName}.validatePayload: No JSON schema found for command ${commandName} PDU validation`
+    );
+    return false;
+  }
+}
diff --git a/src/charging-station/ocpp/2.0/OCPP20ServiceUtils.ts b/src/charging-station/ocpp/2.0/OCPP20ServiceUtils.ts
new file mode 100644 (file)
index 0000000..7ff5844
--- /dev/null
@@ -0,0 +1,5 @@
+// Partial Copyright Jerome Benoit. 2021. All Rights Reserved.
+
+import { OCPPServiceUtils } from '../OCPPServiceUtils';
+
+export class OCPP20ServiceUtils extends OCPPServiceUtils {}
index 72f96696905165847155a305ddb8b4d5e3f8ab5b..37460aee97cb0382cc322a4f8745d77204045d1e 100644 (file)
@@ -1,4 +1,5 @@
 import type { CircularArray } from '../utils/CircularArray';
+import type { IncomingRequestCommand, RequestCommand } from './ocpp/Requests';
 import type { WorkerData } from './Worker';
 
 export type TimeSeries = {
index 20d1e4498bccc65ca403e5153a6a2f749d69e4d8..97dccc1e1480418bb4f8740be23da78252be953b 100644 (file)
@@ -18,6 +18,23 @@ export enum OCPP16RequestCommand {
   DATA_TRANSFER = 'DataTransfer',
 }
 
+export enum OCPP16IncomingRequestCommand {
+  RESET = 'Reset',
+  CLEAR_CACHE = 'ClearCache',
+  CHANGE_AVAILABILITY = 'ChangeAvailability',
+  UNLOCK_CONNECTOR = 'UnlockConnector',
+  GET_CONFIGURATION = 'GetConfiguration',
+  CHANGE_CONFIGURATION = 'ChangeConfiguration',
+  SET_CHARGING_PROFILE = 'SetChargingProfile',
+  CLEAR_CHARGING_PROFILE = 'ClearChargingProfile',
+  REMOTE_START_TRANSACTION = 'RemoteStartTransaction',
+  REMOTE_STOP_TRANSACTION = 'RemoteStopTransaction',
+  GET_DIAGNOSTICS = 'GetDiagnostics',
+  TRIGGER_MESSAGE = 'TriggerMessage',
+  DATA_TRANSFER = 'DataTransfer',
+  UPDATE_FIRMWARE = 'UpdateFirmware',
+}
+
 export type OCPP16HeartbeatRequest = EmptyObject;
 
 export interface OCPP16BootNotificationRequest extends JsonObject {
@@ -42,23 +59,6 @@ export interface OCPP16StatusNotificationRequest extends JsonObject {
   vendorErrorCode?: string;
 }
 
-export enum OCPP16IncomingRequestCommand {
-  RESET = 'Reset',
-  CLEAR_CACHE = 'ClearCache',
-  CHANGE_AVAILABILITY = 'ChangeAvailability',
-  UNLOCK_CONNECTOR = 'UnlockConnector',
-  GET_CONFIGURATION = 'GetConfiguration',
-  CHANGE_CONFIGURATION = 'ChangeConfiguration',
-  SET_CHARGING_PROFILE = 'SetChargingProfile',
-  CLEAR_CHARGING_PROFILE = 'ClearChargingProfile',
-  REMOTE_START_TRANSACTION = 'RemoteStartTransaction',
-  REMOTE_STOP_TRANSACTION = 'RemoteStopTransaction',
-  GET_DIAGNOSTICS = 'GetDiagnostics',
-  TRIGGER_MESSAGE = 'TriggerMessage',
-  DATA_TRANSFER = 'DataTransfer',
-  UPDATE_FIRMWARE = 'UpdateFirmware',
-}
-
 export type OCPP16ClearCacheRequest = EmptyObject;
 
 export interface ChangeConfigurationRequest extends JsonObject {
diff --git a/src/types/ocpp/2.0/Requests.ts b/src/types/ocpp/2.0/Requests.ts
new file mode 100644 (file)
index 0000000..04c6c91
--- /dev/null
@@ -0,0 +1,3 @@
+export enum OCPP20RequestCommand {}
+
+export enum OCPP20IncomingRequestCommand {}
diff --git a/src/types/ocpp/2.0/Responses.ts b/src/types/ocpp/2.0/Responses.ts
new file mode 100644 (file)
index 0000000..e69de29
index 1aa015b6c956500e953a0d3bc23caa590f244dbe..871015b6078855c30132ee27ac01f19dffaf5635 100644 (file)
@@ -5,20 +5,22 @@ import { OCPP16DiagnosticsStatus } from './1.6/DiagnosticsStatus';
 import type { OCPP16MeterValuesRequest } from './1.6/MeterValues';
 import {
   OCPP16AvailabilityType,
-  OCPP16BootNotificationRequest,
-  OCPP16DataTransferRequest,
-  OCPP16HeartbeatRequest,
+  type OCPP16BootNotificationRequest,
+  type OCPP16DataTransferRequest,
+  type OCPP16HeartbeatRequest,
   OCPP16IncomingRequestCommand,
   OCPP16MessageTrigger,
   OCPP16RequestCommand,
-  OCPP16StatusNotificationRequest,
+  type OCPP16StatusNotificationRequest,
 } from './1.6/Requests';
+import { OCPP20IncomingRequestCommand, OCPP20RequestCommand } from './2.0/Requests';
 import type { MessageType } from './MessageType';
 
 export type RequestCommand = OCPP16RequestCommand;
 
 export const RequestCommand = {
   ...OCPP16RequestCommand,
+  ...OCPP20RequestCommand,
 };
 
 export type OutgoingRequest = [MessageType.CALL_MESSAGE, string, RequestCommand, JsonType];
@@ -32,6 +34,7 @@ export type IncomingRequestCommand = OCPP16IncomingRequestCommand;
 
 export const IncomingRequestCommand = {
   ...OCPP16IncomingRequestCommand,
+  ...OCPP20IncomingRequestCommand,
 };
 
 export type IncomingRequest = [MessageType.CALL_MESSAGE, string, IncomingRequestCommand, JsonType];