From 953d6b028e82d6997897802afefbb4ce0d225dee Mon Sep 17 00:00:00 2001 From: =?utf8?q?J=C3=A9r=C3=B4me=20Benoit?= Date: Wed, 4 Jan 2023 20:42:07 +0100 Subject: [PATCH] Add initial classes structure for the OCPP 2.0 stack MIME-Version: 1.0 Content-Type: text/plain; charset=utf8 Content-Transfer-Encoding: 8bit Signed-off-by: Jérôme Benoit --- .../ocpp/1.6/OCPP16ServiceUtils.ts | 2 +- .../ocpp/2.0/OCPP20IncomingRequestService.ts | 131 ++++++++++++++++++ .../ocpp/2.0/OCPP20RequestService.ts | 99 +++++++++++++ .../ocpp/2.0/OCPP20ResponseService.ts | 100 +++++++++++++ .../ocpp/2.0/OCPP20ServiceUtils.ts | 5 + src/types/Statistics.ts | 1 + src/types/ocpp/1.6/Requests.ts | 34 ++--- src/types/ocpp/2.0/Requests.ts | 3 + src/types/ocpp/2.0/Responses.ts | 0 src/types/ocpp/Requests.ts | 11 +- 10 files changed, 364 insertions(+), 22 deletions(-) create mode 100644 src/charging-station/ocpp/2.0/OCPP20IncomingRequestService.ts create mode 100644 src/charging-station/ocpp/2.0/OCPP20RequestService.ts create mode 100644 src/charging-station/ocpp/2.0/OCPP20ResponseService.ts create mode 100644 src/charging-station/ocpp/2.0/OCPP20ServiceUtils.ts create mode 100644 src/types/ocpp/2.0/Requests.ts create mode 100644 src/types/ocpp/2.0/Responses.ts diff --git a/src/charging-station/ocpp/1.6/OCPP16ServiceUtils.ts b/src/charging-station/ocpp/1.6/OCPP16ServiceUtils.ts index bb129482..f3f22e45 100644 --- a/src/charging-station/ocpp/1.6/OCPP16ServiceUtils.ts +++ b/src/charging-station/ocpp/1.6/OCPP16ServiceUtils.ts @@ -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 index 00000000..076832af --- /dev/null +++ b/src/charging-station/ocpp/2.0/OCPP20IncomingRequestService.ts @@ -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; + private jsonSchemas: Map>; + + public constructor() { + if (new.target?.name === moduleName) { + throw new TypeError(`Cannot construct ${new.target?.name} instances directly`); + } + super(); + this.incomingRequestHandlers = new Map(); + this.jsonSchemas = new Map>(); + this.validatePayload.bind(this); + } + + public async incomingRequestHandler( + chargingStation: ChargingStation, + messageId: string, + commandName: OCPP20IncomingRequestCommand, + commandPayload: JsonType + ): Promise { + 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 index 00000000..8dfdce68 --- /dev/null +++ b/src/charging-station/ocpp/2.0/OCPP20RequestService.ts @@ -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>; + + 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>(); + this.buildRequestPayload.bind(this); + this.validatePayload.bind(this); + } + + public async requestHandler( + chargingStation: ChargingStation, + commandName: OCPP20RequestCommand, + commandParams?: JsonType, + params?: RequestParams + ): Promise { + if (OCPP20ServiceUtils.isRequestCommandSupported(chargingStation, commandName) === true) { + const requestPayload = this.buildRequestPayload( + 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( + 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( + 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 index 00000000..db320f61 --- /dev/null +++ b/src/charging-station/ocpp/2.0/OCPP20ResponseService.ts @@ -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; + private jsonSchemas: Map>; + + public constructor() { + if (new.target?.name === moduleName) { + throw new TypeError(`Cannot construct ${new.target?.name} instances directly`); + } + super(); + this.responseHandlers = new Map(); + this.jsonSchemas = new Map>(); + this.validatePayload.bind(this); + } + + public async responseHandler( + chargingStation: ChargingStation, + commandName: OCPP20RequestCommand, + payload: JsonType, + requestPayload: JsonType + ): Promise { + 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 index 00000000..7ff58444 --- /dev/null +++ b/src/charging-station/ocpp/2.0/OCPP20ServiceUtils.ts @@ -0,0 +1,5 @@ +// Partial Copyright Jerome Benoit. 2021. All Rights Reserved. + +import { OCPPServiceUtils } from '../OCPPServiceUtils'; + +export class OCPP20ServiceUtils extends OCPPServiceUtils {} diff --git a/src/types/Statistics.ts b/src/types/Statistics.ts index 72f96696..37460aee 100644 --- a/src/types/Statistics.ts +++ b/src/types/Statistics.ts @@ -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 = { diff --git a/src/types/ocpp/1.6/Requests.ts b/src/types/ocpp/1.6/Requests.ts index 20d1e449..97dccc1e 100644 --- a/src/types/ocpp/1.6/Requests.ts +++ b/src/types/ocpp/1.6/Requests.ts @@ -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 index 00000000..04c6c91d --- /dev/null +++ b/src/types/ocpp/2.0/Requests.ts @@ -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 index 00000000..e69de29b diff --git a/src/types/ocpp/Requests.ts b/src/types/ocpp/Requests.ts index 1aa015b6..871015b6 100644 --- a/src/types/ocpp/Requests.ts +++ b/src/types/ocpp/Requests.ts @@ -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]; -- 2.34.1