X-Git-Url: https://git.piment-noir.org/?a=blobdiff_plain;f=src%2Fcharging-station%2Focpp%2F1.6%2FOCPP16IncomingRequestService.ts;h=8c6ec224f1edf91b7945ed6939bdbd07857850f1;hb=bcf95df12dfe4236db8c278eabea9f5058f40933;hp=e2d5886bc799001d36837bef11bff647a2850297;hpb=721646e902fa12d165d4a1da06fb963fb30dc9f2;p=e-mobility-charging-stations-simulator.git diff --git a/src/charging-station/ocpp/1.6/OCPP16IncomingRequestService.ts b/src/charging-station/ocpp/1.6/OCPP16IncomingRequestService.ts index e2d5886b..c8bc557c 100644 --- a/src/charging-station/ocpp/1.6/OCPP16IncomingRequestService.ts +++ b/src/charging-station/ocpp/1.6/OCPP16IncomingRequestService.ts @@ -1,49 +1,63 @@ -// Partial Copyright Jerome Benoit. 2021-2023. All Rights Reserved. +// Partial Copyright Jerome Benoit. 2021-2024. All Rights Reserved. -import fs from 'node:fs'; -import path from 'node:path'; -import { URL, fileURLToPath } from 'node:url'; +import { createWriteStream, readdirSync } from 'node:fs' +import { dirname, join, resolve } from 'node:path' +import { URL, fileURLToPath } from 'node:url' -import type { JSONSchemaType } from 'ajv'; -import { Client, type FTPResponse } from 'basic-ftp'; -import tar from 'tar'; +import type { ValidateFunction } from 'ajv' +import { Client, type FTPResponse } from 'basic-ftp' +import { + type Interval, + addSeconds, + differenceInSeconds, + isDate, + secondsToMilliseconds +} from 'date-fns' +import { maxTime } from 'date-fns/constants' +import { create } from 'tar' +import { OCPP16Constants } from './OCPP16Constants.js' +import { OCPP16ServiceUtils } from './OCPP16ServiceUtils.js' import { type ChargingStation, - ChargingStationConfigurationUtils, - ChargingStationUtils, -} from '../../../charging-station'; -import { OCPPError } from '../../../exception'; + canProceedChargingProfile, + checkChargingStation, + getConfigurationKey, + getConnectorChargingProfiles, + prepareChargingProfileKind, + removeExpiredReservations, + setConfigurationKeyValue +} from '../../../charging-station/index.js' +import { OCPPError } from '../../../exception/index.js' import { - type ChangeAvailabilityRequest, - type ChangeAvailabilityResponse, type ChangeConfigurationRequest, type ChangeConfigurationResponse, - type ClearChargingProfileRequest, - type ClearChargingProfileResponse, ErrorType, type GenericResponse, + GenericStatus, type GetConfigurationRequest, type GetConfigurationResponse, type GetDiagnosticsRequest, type GetDiagnosticsResponse, type IncomingRequestHandler, - type JsonObject, type JsonType, OCPP16AuthorizationStatus, - type OCPP16AuthorizeRequest, - type OCPP16AuthorizeResponse, OCPP16AvailabilityType, type OCPP16BootNotificationRequest, type OCPP16BootNotificationResponse, + type OCPP16CancelReservationRequest, + type OCPP16ChangeAvailabilityRequest, + type OCPP16ChangeAvailabilityResponse, OCPP16ChargePointErrorCode, OCPP16ChargePointStatus, type OCPP16ChargingProfile, OCPP16ChargingProfilePurposeType, + type OCPP16ChargingSchedule, type OCPP16ClearCacheRequest, + type OCPP16ClearChargingProfileRequest, + type OCPP16ClearChargingProfileResponse, type OCPP16DataTransferRequest, type OCPP16DataTransferResponse, - OCPP16DataTransferStatus, OCPP16DataTransferVendorId, OCPP16DiagnosticsStatus, type OCPP16DiagnosticsStatusNotificationRequest, @@ -51,11 +65,15 @@ import { OCPP16FirmwareStatus, type OCPP16FirmwareStatusNotificationRequest, type OCPP16FirmwareStatusNotificationResponse, + type OCPP16GetCompositeScheduleRequest, + type OCPP16GetCompositeScheduleResponse, type OCPP16HeartbeatRequest, type OCPP16HeartbeatResponse, OCPP16IncomingRequestCommand, OCPP16MessageTrigger, OCPP16RequestCommand, + type OCPP16ReserveNowRequest, + type OCPP16ReserveNowResponse, OCPP16StandardParametersKey, type OCPP16StartTransactionRequest, type OCPP16StartTransactionResponse, @@ -65,196 +83,521 @@ import { OCPP16SupportedFeatureProfiles, type OCPP16TriggerMessageRequest, type OCPP16TriggerMessageResponse, + OCPP16TriggerMessageStatus, type OCPP16UpdateFirmwareRequest, type OCPP16UpdateFirmwareResponse, type OCPPConfigurationKey, OCPPVersion, type RemoteStartTransactionRequest, type RemoteStopTransactionRequest, + ReservationTerminationReason, type ResetRequest, type SetChargingProfileRequest, type SetChargingProfileResponse, type UnlockConnectorRequest, - type UnlockConnectorResponse, -} from '../../../types'; -import { Constants, Utils, logger } from '../../../utils'; -import { OCPP16ServiceUtils, OCPPConstants, OCPPIncomingRequestService } from '../internal'; + type UnlockConnectorResponse +} from '../../../types/index.js' +import { + Constants, + convertToDate, + convertToInt, + formatDurationMilliSeconds, + getRandomInteger, + isAsyncFunction, + isEmptyArray, + isNotEmptyArray, + isNotEmptyString, + logger, + sleep +} from '../../../utils/index.js' +import { OCPPIncomingRequestService } from '../OCPPIncomingRequestService.js' -const moduleName = 'OCPP16IncomingRequestService'; +const moduleName = 'OCPP16IncomingRequestService' export class OCPP16IncomingRequestService extends OCPPIncomingRequestService { - protected jsonSchemas: Map>; - private incomingRequestHandlers: Map; + protected payloadValidateFunctions: Map> - public constructor() { - if (new.target?.name === moduleName) { - throw new TypeError(`Cannot construct ${new.target?.name} instances directly`); - } - super(OCPPVersion.VERSION_16); + private readonly incomingRequestHandlers: Map< + OCPP16IncomingRequestCommand, + IncomingRequestHandler + > + + public constructor () { + // if (new.target.name === moduleName) { + // throw new TypeError(`Cannot construct ${new.target.name} instances directly`) + // } + super(OCPPVersion.VERSION_16) this.incomingRequestHandlers = new Map([ - [OCPP16IncomingRequestCommand.RESET, this.handleRequestReset.bind(this)], - [OCPP16IncomingRequestCommand.CLEAR_CACHE, this.handleRequestClearCache.bind(this)], - [OCPP16IncomingRequestCommand.UNLOCK_CONNECTOR, this.handleRequestUnlockConnector.bind(this)], + [ + OCPP16IncomingRequestCommand.RESET, + this.handleRequestReset.bind(this) as unknown as IncomingRequestHandler + ], + [ + OCPP16IncomingRequestCommand.CLEAR_CACHE, + this.handleRequestClearCache.bind(this) as IncomingRequestHandler + ], + [ + OCPP16IncomingRequestCommand.UNLOCK_CONNECTOR, + this.handleRequestUnlockConnector.bind(this) as unknown as IncomingRequestHandler + ], [ OCPP16IncomingRequestCommand.GET_CONFIGURATION, - this.handleRequestGetConfiguration.bind(this), + this.handleRequestGetConfiguration.bind(this) as IncomingRequestHandler ], [ OCPP16IncomingRequestCommand.CHANGE_CONFIGURATION, - this.handleRequestChangeConfiguration.bind(this), + this.handleRequestChangeConfiguration.bind(this) as unknown as IncomingRequestHandler + ], + [ + OCPP16IncomingRequestCommand.GET_COMPOSITE_SCHEDULE, + this.handleRequestGetCompositeSchedule.bind(this) as unknown as IncomingRequestHandler ], [ OCPP16IncomingRequestCommand.SET_CHARGING_PROFILE, - this.handleRequestSetChargingProfile.bind(this), + this.handleRequestSetChargingProfile.bind(this) as unknown as IncomingRequestHandler ], [ OCPP16IncomingRequestCommand.CLEAR_CHARGING_PROFILE, - this.handleRequestClearChargingProfile.bind(this), + this.handleRequestClearChargingProfile.bind(this) as IncomingRequestHandler ], [ OCPP16IncomingRequestCommand.CHANGE_AVAILABILITY, - this.handleRequestChangeAvailability.bind(this), + this.handleRequestChangeAvailability.bind(this) as unknown as IncomingRequestHandler ], [ OCPP16IncomingRequestCommand.REMOTE_START_TRANSACTION, - this.handleRequestRemoteStartTransaction.bind(this), + this.handleRequestRemoteStartTransaction.bind(this) as unknown as IncomingRequestHandler ], [ OCPP16IncomingRequestCommand.REMOTE_STOP_TRANSACTION, - this.handleRequestRemoteStopTransaction.bind(this), + this.handleRequestRemoteStopTransaction.bind(this) as unknown as IncomingRequestHandler + ], + [ + OCPP16IncomingRequestCommand.GET_DIAGNOSTICS, + this.handleRequestGetDiagnostics.bind(this) as IncomingRequestHandler + ], + [ + OCPP16IncomingRequestCommand.TRIGGER_MESSAGE, + this.handleRequestTriggerMessage.bind(this) as unknown as IncomingRequestHandler + ], + [ + OCPP16IncomingRequestCommand.DATA_TRANSFER, + this.handleRequestDataTransfer.bind(this) as unknown as IncomingRequestHandler + ], + [ + OCPP16IncomingRequestCommand.UPDATE_FIRMWARE, + this.handleRequestUpdateFirmware.bind(this) as unknown as IncomingRequestHandler ], - [OCPP16IncomingRequestCommand.GET_DIAGNOSTICS, this.handleRequestGetDiagnostics.bind(this)], - [OCPP16IncomingRequestCommand.TRIGGER_MESSAGE, this.handleRequestTriggerMessage.bind(this)], - [OCPP16IncomingRequestCommand.DATA_TRANSFER, this.handleRequestDataTransfer.bind(this)], - [OCPP16IncomingRequestCommand.UPDATE_FIRMWARE, this.handleRequestUpdateFirmware.bind(this)], - ]); - this.jsonSchemas = new Map>([ + [ + OCPP16IncomingRequestCommand.RESERVE_NOW, + this.handleRequestReserveNow.bind(this) as unknown as IncomingRequestHandler + ], + [ + OCPP16IncomingRequestCommand.CANCEL_RESERVATION, + this.handleRequestCancelReservation.bind(this) as unknown as IncomingRequestHandler + ] + ]) + this.payloadValidateFunctions = new Map< + OCPP16IncomingRequestCommand, + ValidateFunction + >([ [ OCPP16IncomingRequestCommand.RESET, - OCPP16ServiceUtils.parseJsonSchemaFile( - '../../../assets/json-schemas/ocpp/1.6/Reset.json', - moduleName, - 'constructor' - ), + this.ajv + .compile( + OCPP16ServiceUtils.parseJsonSchemaFile( + 'assets/json-schemas/ocpp/1.6/Reset.json', + moduleName, + 'constructor' + ) + ) + .bind(this) ], [ OCPP16IncomingRequestCommand.CLEAR_CACHE, - OCPP16ServiceUtils.parseJsonSchemaFile( - '../../../assets/json-schemas/ocpp/1.6/ClearCache.json', - moduleName, - 'constructor' - ), + this.ajv + .compile( + OCPP16ServiceUtils.parseJsonSchemaFile( + 'assets/json-schemas/ocpp/1.6/ClearCache.json', + moduleName, + 'constructor' + ) + ) + .bind(this) ], [ OCPP16IncomingRequestCommand.UNLOCK_CONNECTOR, - OCPP16ServiceUtils.parseJsonSchemaFile( - '../../../assets/json-schemas/ocpp/1.6/UnlockConnector.json', - moduleName, - 'constructor' - ), + this.ajv + .compile( + OCPP16ServiceUtils.parseJsonSchemaFile( + 'assets/json-schemas/ocpp/1.6/UnlockConnector.json', + moduleName, + 'constructor' + ) + ) + .bind(this) ], [ OCPP16IncomingRequestCommand.GET_CONFIGURATION, - OCPP16ServiceUtils.parseJsonSchemaFile( - '../../../assets/json-schemas/ocpp/1.6/GetConfiguration.json', - moduleName, - 'constructor' - ), + this.ajv + .compile( + OCPP16ServiceUtils.parseJsonSchemaFile( + 'assets/json-schemas/ocpp/1.6/GetConfiguration.json', + moduleName, + 'constructor' + ) + ) + .bind(this) ], [ OCPP16IncomingRequestCommand.CHANGE_CONFIGURATION, - OCPP16ServiceUtils.parseJsonSchemaFile( - '../../../assets/json-schemas/ocpp/1.6/ChangeConfiguration.json', - moduleName, - 'constructor' - ), + this.ajv + .compile( + OCPP16ServiceUtils.parseJsonSchemaFile( + 'assets/json-schemas/ocpp/1.6/ChangeConfiguration.json', + moduleName, + 'constructor' + ) + ) + .bind(this) ], [ OCPP16IncomingRequestCommand.GET_DIAGNOSTICS, - OCPP16ServiceUtils.parseJsonSchemaFile( - '../../../assets/json-schemas/ocpp/1.6/GetDiagnostics.json', - moduleName, - 'constructor' - ), + this.ajv + .compile( + OCPP16ServiceUtils.parseJsonSchemaFile( + 'assets/json-schemas/ocpp/1.6/GetDiagnostics.json', + moduleName, + 'constructor' + ) + ) + .bind(this) + ], + [ + OCPP16IncomingRequestCommand.GET_COMPOSITE_SCHEDULE, + this.ajv + .compile( + OCPP16ServiceUtils.parseJsonSchemaFile( + 'assets/json-schemas/ocpp/1.6/GetCompositeSchedule.json', + moduleName, + 'constructor' + ) + ) + .bind(this) ], [ OCPP16IncomingRequestCommand.SET_CHARGING_PROFILE, - OCPP16ServiceUtils.parseJsonSchemaFile( - '../../../assets/json-schemas/ocpp/1.6/SetChargingProfile.json', - moduleName, - 'constructor' - ), + this.ajv + .compile( + OCPP16ServiceUtils.parseJsonSchemaFile( + 'assets/json-schemas/ocpp/1.6/SetChargingProfile.json', + moduleName, + 'constructor' + ) + ) + .bind(this) ], [ OCPP16IncomingRequestCommand.CLEAR_CHARGING_PROFILE, - OCPP16ServiceUtils.parseJsonSchemaFile( - '../../../assets/json-schemas/ocpp/1.6/ClearChargingProfile.json', - moduleName, - 'constructor' - ), + this.ajv + .compile( + OCPP16ServiceUtils.parseJsonSchemaFile( + 'assets/json-schemas/ocpp/1.6/ClearChargingProfile.json', + moduleName, + 'constructor' + ) + ) + .bind(this) ], [ OCPP16IncomingRequestCommand.CHANGE_AVAILABILITY, - OCPP16ServiceUtils.parseJsonSchemaFile( - '../../../assets/json-schemas/ocpp/1.6/ChangeAvailability.json', - moduleName, - 'constructor' - ), + this.ajv + .compile( + OCPP16ServiceUtils.parseJsonSchemaFile( + 'assets/json-schemas/ocpp/1.6/ChangeAvailability.json', + moduleName, + 'constructor' + ) + ) + .bind(this) ], [ OCPP16IncomingRequestCommand.REMOTE_START_TRANSACTION, - OCPP16ServiceUtils.parseJsonSchemaFile( - '../../../assets/json-schemas/ocpp/1.6/RemoteStartTransaction.json', - moduleName, - 'constructor' - ), + this.ajv + .compile( + OCPP16ServiceUtils.parseJsonSchemaFile( + 'assets/json-schemas/ocpp/1.6/RemoteStartTransaction.json', + moduleName, + 'constructor' + ) + ) + .bind(this) ], [ OCPP16IncomingRequestCommand.REMOTE_STOP_TRANSACTION, - OCPP16ServiceUtils.parseJsonSchemaFile( - '../../../assets/json-schemas/ocpp/1.6/RemoteStopTransaction.json', - moduleName, - 'constructor' - ), + this.ajv + .compile( + OCPP16ServiceUtils.parseJsonSchemaFile( + 'assets/json-schemas/ocpp/1.6/RemoteStopTransaction.json', + moduleName, + 'constructor' + ) + ) + .bind(this) ], [ OCPP16IncomingRequestCommand.TRIGGER_MESSAGE, - OCPP16ServiceUtils.parseJsonSchemaFile( - '../../../assets/json-schemas/ocpp/1.6/TriggerMessage.json', - moduleName, - 'constructor' - ), + this.ajv + .compile( + OCPP16ServiceUtils.parseJsonSchemaFile( + 'assets/json-schemas/ocpp/1.6/TriggerMessage.json', + moduleName, + 'constructor' + ) + ) + .bind(this) ], [ OCPP16IncomingRequestCommand.DATA_TRANSFER, - OCPP16ServiceUtils.parseJsonSchemaFile( - '../../../assets/json-schemas/ocpp/1.6/DataTransfer.json', - moduleName, - 'constructor' - ), + this.ajv + .compile( + OCPP16ServiceUtils.parseJsonSchemaFile( + 'assets/json-schemas/ocpp/1.6/DataTransfer.json', + moduleName, + 'constructor' + ) + ) + .bind(this) ], [ OCPP16IncomingRequestCommand.UPDATE_FIRMWARE, - OCPP16ServiceUtils.parseJsonSchemaFile( - '../../../assets/json-schemas/ocpp/1.6/UpdateFirmware.json', - moduleName, - 'constructor' - ), + this.ajv + .compile( + OCPP16ServiceUtils.parseJsonSchemaFile( + 'assets/json-schemas/ocpp/1.6/UpdateFirmware.json', + moduleName, + 'constructor' + ) + ) + .bind(this) + ], + [ + OCPP16IncomingRequestCommand.RESERVE_NOW, + this.ajv + .compile( + OCPP16ServiceUtils.parseJsonSchemaFile( + 'assets/json-schemas/ocpp/1.6/ReserveNow.json', + moduleName, + 'constructor' + ) + ) + .bind(this) ], - ]); - this.validatePayload.bind(this); + [ + OCPP16IncomingRequestCommand.CANCEL_RESERVATION, + this.ajv + .compile( + OCPP16ServiceUtils.parseJsonSchemaFile( + 'assets/json-schemas/ocpp/1.6/CancelReservation.json', + moduleName, + 'constructor' + ) + ) + .bind(this) + ] + ]) + // Handle incoming request events + this.on( + OCPP16IncomingRequestCommand.REMOTE_START_TRANSACTION, + ( + chargingStation: ChargingStation, + request: RemoteStartTransactionRequest, + response: GenericResponse + ) => { + if (response.status === GenericStatus.Accepted) { + const { connectorId, idTag } = request + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion + chargingStation.getConnectorStatus(connectorId)!.transactionRemoteStarted = true + chargingStation.ocppRequestService + .requestHandler( + chargingStation, + OCPP16RequestCommand.START_TRANSACTION, + { + connectorId, + idTag + } + ) + .then(response => { + if (response.status === OCPP16AuthorizationStatus.ACCEPTED) { + logger.debug( + `${chargingStation.logPrefix()} Remote start transaction ACCEPTED on ${chargingStation.stationInfo?.chargingStationId}#${connectorId} for idTag '${idTag}'` + ) + } else { + logger.debug( + `${chargingStation.logPrefix()} Remote start transaction REJECTED on ${chargingStation.stationInfo?.chargingStationId}#${connectorId} for idTag '${idTag}'` + ) + } + }) + .catch(error => { + logger.error( + `${chargingStation.logPrefix()} ${moduleName}.constructor: Remote start transaction error:`, + error + ) + }) + } + } + ) + this.on( + OCPP16IncomingRequestCommand.REMOTE_STOP_TRANSACTION, + ( + chargingStation: ChargingStation, + request: RemoteStopTransactionRequest, + response: GenericResponse + ) => { + if (response.status === GenericStatus.Accepted) { + const { transactionId } = request + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion + const connectorId = chargingStation.getConnectorIdByTransactionId(transactionId)! + OCPP16ServiceUtils.remoteStopTransaction(chargingStation, connectorId) + .then(response => { + if (response.status === GenericStatus.Accepted) { + logger.debug( + `${chargingStation.logPrefix()} Remote stop transaction ACCEPTED on ${chargingStation.stationInfo?.chargingStationId}#${connectorId} for transaction '${transactionId}'` + ) + } else { + logger.debug( + `${chargingStation.logPrefix()} Remote stop transaction REJECTED on ${chargingStation.stationInfo?.chargingStationId}#${connectorId} for transaction '${transactionId}'` + ) + } + }) + .catch(error => { + logger.error( + `${chargingStation.logPrefix()} ${moduleName}.constructor: Remote stop transaction error:`, + error + ) + }) + } + } + ) + this.on( + OCPP16IncomingRequestCommand.TRIGGER_MESSAGE, + ( + chargingStation: ChargingStation, + request: OCPP16TriggerMessageRequest, + response: OCPP16TriggerMessageResponse + ) => { + if (response.status !== OCPP16TriggerMessageStatus.ACCEPTED) { + return + } + const { requestedMessage, connectorId } = request + const errorHandler = (error: Error): void => { + logger.error( + `${chargingStation.logPrefix()} ${moduleName}.constructor: Trigger ${requestedMessage} error:`, + error + ) + } + switch (requestedMessage) { + case OCPP16MessageTrigger.BootNotification: + chargingStation.ocppRequestService + .requestHandler( + chargingStation, + OCPP16RequestCommand.BOOT_NOTIFICATION, + chargingStation.bootNotificationRequest, + { skipBufferingOnError: true, triggerMessage: true } + ) + .then(response => { + chargingStation.bootNotificationResponse = response + }) + .catch(errorHandler) + break + case OCPP16MessageTrigger.Heartbeat: + chargingStation.ocppRequestService + .requestHandler( + chargingStation, + OCPP16RequestCommand.HEARTBEAT, + undefined, + { + triggerMessage: true + } + ) + .catch(errorHandler) + break + case OCPP16MessageTrigger.StatusNotification: + if (connectorId != null) { + chargingStation.ocppRequestService + .requestHandler( + chargingStation, + OCPP16RequestCommand.STATUS_NOTIFICATION, + { + connectorId, + errorCode: OCPP16ChargePointErrorCode.NO_ERROR, + status: chargingStation.getConnectorStatus(connectorId)?.status + }, + { + triggerMessage: true + } + ) + .catch(errorHandler) + } else if (chargingStation.hasEvses) { + for (const evseStatus of chargingStation.evses.values()) { + for (const [id, connectorStatus] of evseStatus.connectors) { + chargingStation.ocppRequestService + .requestHandler< + OCPP16StatusNotificationRequest, + OCPP16StatusNotificationResponse + >( + chargingStation, + OCPP16RequestCommand.STATUS_NOTIFICATION, + { + connectorId: id, + errorCode: OCPP16ChargePointErrorCode.NO_ERROR, + status: connectorStatus.status + }, + { + triggerMessage: true + } + ) + .catch(errorHandler) + } + } + } else { + for (const [id, connectorStatus] of chargingStation.connectors) { + chargingStation.ocppRequestService + .requestHandler< + OCPP16StatusNotificationRequest, + OCPP16StatusNotificationResponse + >( + chargingStation, + OCPP16RequestCommand.STATUS_NOTIFICATION, + { + connectorId: id, + errorCode: OCPP16ChargePointErrorCode.NO_ERROR, + status: connectorStatus.status + }, + { + triggerMessage: true + } + ) + .catch(errorHandler) + } + } + break + } + } + ) + this.validatePayload = this.validatePayload.bind(this) } - public async incomingRequestHandler( + public async incomingRequestHandler( chargingStation: ChargingStation, messageId: string, commandName: OCPP16IncomingRequestCommand, - commandPayload: JsonType + commandPayload: ReqType ): Promise { - let response: JsonType; + let response: ResType if ( - chargingStation.getOcppStrictCompliance() === true && - chargingStation.isInPendingState() === true && + chargingStation.stationInfo?.ocppStrictCompliance === true && + chargingStation.inPendingState() && (commandName === OCPP16IncomingRequestCommand.REMOTE_START_TRANSACTION || commandName === OCPP16IncomingRequestCommand.REMOTE_STOP_TRANSACTION) ) { @@ -262,36 +605,39 @@ export class OCPP16IncomingRequestService extends OCPPIncomingRequestService { ErrorType.SECURITY_ERROR, `${commandName} cannot be issued to handle request PDU ${JSON.stringify( commandPayload, - null, + undefined, 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) + chargingStation.isRegistered() || + (chargingStation.stationInfo?.ocppStrictCompliance === false && + chargingStation.inUnknownState()) ) { if ( - this.incomingRequestHandlers.has(commandName) === true && - OCPP16ServiceUtils.isIncomingRequestCommandSupported(chargingStation, commandName) === true + this.incomingRequestHandlers.has(commandName) && + OCPP16ServiceUtils.isIncomingRequestCommandSupported(chargingStation, commandName) ) { try { - this.validatePayload(chargingStation, commandName, commandPayload); + this.validatePayload(chargingStation, commandName, commandPayload) // Call the method to build the response - response = await this.incomingRequestHandlers.get(commandName)( - chargingStation, - commandPayload - ); + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion + const incomingRequestHandler = this.incomingRequestHandlers.get(commandName)! + if (isAsyncFunction(incomingRequestHandler)) { + response = (await incomingRequestHandler(chargingStation, commandPayload)) as ResType + } else { + response = incomingRequestHandler(chargingStation, commandPayload) as ResType + } } catch (error) { // Log logger.error( `${chargingStation.logPrefix()} ${moduleName}.incomingRequestHandler: Handle incoming request error:`, error - ); - throw error; + ) + throw error } } else { // Throw exception @@ -299,24 +645,24 @@ export class OCPP16IncomingRequestService extends OCPPIncomingRequestService { ErrorType.NOT_IMPLEMENTED, `${commandName} is not implemented to handle request PDU ${JSON.stringify( commandPayload, - null, + undefined, 2 )}`, commandName, commandPayload - ); + ) } } else { throw new OCPPError( ErrorType.SECURITY_ERROR, `${commandName} cannot be issued to handle request PDU ${JSON.stringify( commandPayload, - null, + undefined, 2 )} while the charging station is not registered on the central server.`, commandName, commandPayload - ); + ) } // Send the built response await chargingStation.ocppRequestService.sendResponse( @@ -324,984 +670,1059 @@ export class OCPP16IncomingRequestService extends OCPPIncomingRequestService { messageId, response, commandName - ); + ) + // Emit command name event to allow delayed handling + this.emit(commandName, chargingStation, commandPayload, response) } - private validatePayload( + private validatePayload ( chargingStation: ChargingStation, commandName: OCPP16IncomingRequestCommand, commandPayload: JsonType ): boolean { - if (this.jsonSchemas.has(commandName) === true) { - return this.validateIncomingRequestPayload( - chargingStation, - commandName, - this.jsonSchemas.get(commandName), - commandPayload - ); + if (this.payloadValidateFunctions.has(commandName)) { + return this.validateIncomingRequestPayload(chargingStation, commandName, commandPayload) } logger.warn( - `${chargingStation.logPrefix()} ${moduleName}.validatePayload: No JSON schema found for command '${commandName}' PDU validation` - ); - return false; + `${chargingStation.logPrefix()} ${moduleName}.validatePayload: No JSON schema validation function found for command '${commandName}' PDU validation` + ) + return false } // Simulate charging station restart - private handleRequestReset( + private handleRequestReset ( chargingStation: ChargingStation, commandPayload: ResetRequest ): GenericResponse { - this.runInAsyncScope( - chargingStation.reset.bind(chargingStation) as ( - this: ChargingStation, - ...args: any[] - ) => Promise, - chargingStation, - `${commandPayload.type}Reset` as OCPP16StopTransactionReason - ).catch(Constants.EMPTY_FUNCTION); + const { type } = commandPayload + chargingStation + .reset(`${type}Reset` as OCPP16StopTransactionReason) + .catch(Constants.EMPTY_FUNCTION) logger.info( - `${chargingStation.logPrefix()} ${ - commandPayload.type - } reset command received, simulating it. The station will be back online in ${Utils.formatDurationMilliSeconds( - chargingStation.stationInfo.resetTime + `${chargingStation.logPrefix()} ${type} reset command received, simulating it. The station will be back online in ${formatDurationMilliSeconds( + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion + chargingStation.stationInfo!.resetTime! )}` - ); - return OCPPConstants.OCPP_RESPONSE_ACCEPTED; + ) + return OCPP16Constants.OCPP_RESPONSE_ACCEPTED } - private async handleRequestUnlockConnector( + private async handleRequestUnlockConnector ( chargingStation: ChargingStation, commandPayload: UnlockConnectorRequest ): Promise { - const connectorId = commandPayload.connectorId; - if (chargingStation.connectors.has(connectorId) === false) { + const { connectorId } = commandPayload + if (!chargingStation.hasConnector(connectorId)) { logger.error( - `${chargingStation.logPrefix()} Trying to unlock a non existing connector Id ${connectorId.toString()}` - ); - return OCPPConstants.OCPP_RESPONSE_UNLOCK_NOT_SUPPORTED; + `${chargingStation.logPrefix()} Trying to unlock a non existing connector id ${connectorId}` + ) + return OCPP16Constants.OCPP_RESPONSE_UNLOCK_NOT_SUPPORTED } if (connectorId === 0) { - logger.error( - `${chargingStation.logPrefix()} Trying to unlock connector Id ${connectorId.toString()}` - ); - return OCPPConstants.OCPP_RESPONSE_UNLOCK_NOT_SUPPORTED; + logger.error(`${chargingStation.logPrefix()} Trying to unlock connector id ${connectorId}`) + return OCPP16Constants.OCPP_RESPONSE_UNLOCK_NOT_SUPPORTED } if (chargingStation.getConnectorStatus(connectorId)?.transactionStarted === true) { const stopResponse = await chargingStation.stopTransactionOnConnector( connectorId, OCPP16StopTransactionReason.UNLOCK_COMMAND - ); + ) if (stopResponse.idTagInfo?.status === OCPP16AuthorizationStatus.ACCEPTED) { - return OCPPConstants.OCPP_RESPONSE_UNLOCKED; + return OCPP16Constants.OCPP_RESPONSE_UNLOCKED } - return OCPPConstants.OCPP_RESPONSE_UNLOCK_FAILED; + return OCPP16Constants.OCPP_RESPONSE_UNLOCK_FAILED } - await chargingStation.ocppRequestService.requestHandler< - OCPP16StatusNotificationRequest, - OCPP16StatusNotificationResponse - >(chargingStation, OCPP16RequestCommand.STATUS_NOTIFICATION, { + await OCPP16ServiceUtils.sendAndSetConnectorStatus( + chargingStation, connectorId, - status: OCPP16ChargePointStatus.Available, - errorCode: OCPP16ChargePointErrorCode.NO_ERROR, - }); - chargingStation.getConnectorStatus(connectorId).status = OCPP16ChargePointStatus.Available; - return OCPPConstants.OCPP_RESPONSE_UNLOCKED; + OCPP16ChargePointStatus.Available + ) + return OCPP16Constants.OCPP_RESPONSE_UNLOCKED } - private handleRequestGetConfiguration( + private handleRequestGetConfiguration ( chargingStation: ChargingStation, commandPayload: GetConfigurationRequest ): GetConfigurationResponse { - const configurationKey: OCPPConfigurationKey[] = []; - const unknownKey: string[] = []; - if (Utils.isUndefined(commandPayload.key) === true) { - for (const configuration of chargingStation.ocppConfiguration.configurationKey) { - if (Utils.isUndefined(configuration.visible) === true) { - configuration.visible = true; - } - if (configuration.visible === false) { - continue; + const { key } = commandPayload + const configurationKey: OCPPConfigurationKey[] = [] + const unknownKey: string[] = [] + if (key == null) { + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion + for (const configKey of chargingStation.ocppConfiguration!.configurationKey!) { + if (!OCPP16ServiceUtils.isConfigurationKeyVisible(configKey)) { + continue } configurationKey.push({ - key: configuration.key, - readonly: configuration.readonly, - value: configuration.value, - }); + key: configKey.key, + readonly: configKey.readonly, + value: configKey.value + }) } - } else if (Utils.isNotEmptyArray(commandPayload.key) === true) { - for (const key of commandPayload.key) { - const keyFound = ChargingStationConfigurationUtils.getConfigurationKey( - chargingStation, - key, - true - ); - if (keyFound) { - if (Utils.isUndefined(keyFound.visible) === true) { - keyFound.visible = true; - } - if (keyFound.visible === false) { - continue; + } else if (isNotEmptyArray(key)) { + for (const k of key) { + const keyFound = getConfigurationKey(chargingStation, k, true) + if (keyFound != null) { + if (!OCPP16ServiceUtils.isConfigurationKeyVisible(keyFound)) { + continue } configurationKey.push({ key: keyFound.key, readonly: keyFound.readonly, - value: keyFound.value, - }); + value: keyFound.value + }) } else { - unknownKey.push(key); + unknownKey.push(k) } } } return { configurationKey, - unknownKey, - }; + unknownKey + } } - private handleRequestChangeConfiguration( + private handleRequestChangeConfiguration ( chargingStation: ChargingStation, commandPayload: ChangeConfigurationRequest ): ChangeConfigurationResponse { - const keyToChange = ChargingStationConfigurationUtils.getConfigurationKey( - chargingStation, - commandPayload.key, - true - ); - if (!keyToChange) { - return OCPPConstants.OCPP_CONFIGURATION_RESPONSE_NOT_SUPPORTED; - } else if (keyToChange && keyToChange.readonly) { - return OCPPConstants.OCPP_CONFIGURATION_RESPONSE_REJECTED; - } else if (keyToChange && !keyToChange.readonly) { - let valueChanged = false; - if (keyToChange.value !== commandPayload.value) { - ChargingStationConfigurationUtils.setConfigurationKeyValue( - chargingStation, - commandPayload.key, - commandPayload.value, - true - ); - valueChanged = true; + const { key, value } = commandPayload + const keyToChange = getConfigurationKey(chargingStation, key, true) + if (keyToChange?.readonly === true) { + return OCPP16Constants.OCPP_CONFIGURATION_RESPONSE_REJECTED + } else if (keyToChange?.readonly === false) { + let valueChanged = false + if (keyToChange.value !== value) { + setConfigurationKeyValue(chargingStation, key, value, true) + valueChanged = true } - let triggerHeartbeatRestart = false; - if (keyToChange.key === OCPP16StandardParametersKey.HeartBeatInterval && valueChanged) { - ChargingStationConfigurationUtils.setConfigurationKeyValue( + let triggerHeartbeatRestart = false + if ( + (keyToChange.key as OCPP16StandardParametersKey) === + OCPP16StandardParametersKey.HeartBeatInterval && + valueChanged + ) { + setConfigurationKeyValue( chargingStation, OCPP16StandardParametersKey.HeartbeatInterval, - commandPayload.value - ); - triggerHeartbeatRestart = true; + value + ) + triggerHeartbeatRestart = true } - if (keyToChange.key === OCPP16StandardParametersKey.HeartbeatInterval && valueChanged) { - ChargingStationConfigurationUtils.setConfigurationKeyValue( + if ( + (keyToChange.key as OCPP16StandardParametersKey) === + OCPP16StandardParametersKey.HeartbeatInterval && + valueChanged + ) { + setConfigurationKeyValue( chargingStation, OCPP16StandardParametersKey.HeartBeatInterval, - commandPayload.value - ); - triggerHeartbeatRestart = true; + value + ) + triggerHeartbeatRestart = true } if (triggerHeartbeatRestart) { - chargingStation.restartHeartbeat(); + chargingStation.restartHeartbeat() } - if (keyToChange.key === OCPP16StandardParametersKey.WebSocketPingInterval && valueChanged) { - chargingStation.restartWebSocketPing(); + if ( + (keyToChange.key as OCPP16StandardParametersKey) === + OCPP16StandardParametersKey.WebSocketPingInterval && + valueChanged + ) { + chargingStation.restartWebSocketPing() } - if (keyToChange.reboot) { - return OCPPConstants.OCPP_CONFIGURATION_RESPONSE_REBOOT_REQUIRED; + if (keyToChange.reboot === true) { + return OCPP16Constants.OCPP_CONFIGURATION_RESPONSE_REBOOT_REQUIRED } - return OCPPConstants.OCPP_CONFIGURATION_RESPONSE_ACCEPTED; + return OCPP16Constants.OCPP_CONFIGURATION_RESPONSE_ACCEPTED } + return OCPP16Constants.OCPP_CONFIGURATION_RESPONSE_NOT_SUPPORTED } - private handleRequestSetChargingProfile( + private handleRequestSetChargingProfile ( chargingStation: ChargingStation, commandPayload: SetChargingProfileRequest ): SetChargingProfileResponse { if ( - OCPP16ServiceUtils.checkFeatureProfile( + !OCPP16ServiceUtils.checkFeatureProfile( chargingStation, OCPP16SupportedFeatureProfiles.SmartCharging, OCPP16IncomingRequestCommand.SET_CHARGING_PROFILE - ) === false + ) ) { - return OCPPConstants.OCPP_SET_CHARGING_PROFILE_RESPONSE_NOT_SUPPORTED; + return OCPP16Constants.OCPP_SET_CHARGING_PROFILE_RESPONSE_NOT_SUPPORTED } - if (chargingStation.connectors.has(commandPayload.connectorId) === false) { + const { connectorId, csChargingProfiles } = commandPayload + if (!chargingStation.hasConnector(connectorId)) { logger.error( - `${chargingStation.logPrefix()} Trying to set charging profile(s) to a non existing connector Id ${ - commandPayload.connectorId - }` - ); - return OCPPConstants.OCPP_SET_CHARGING_PROFILE_RESPONSE_REJECTED; + `${chargingStation.logPrefix()} Trying to set charging profile(s) to a non existing connector id ${connectorId}` + ) + return OCPP16Constants.OCPP_SET_CHARGING_PROFILE_RESPONSE_REJECTED } if ( - commandPayload.csChargingProfiles.chargingProfilePurpose === + csChargingProfiles.chargingProfilePurpose === OCPP16ChargingProfilePurposeType.CHARGE_POINT_MAX_PROFILE && - commandPayload.connectorId !== 0 + connectorId !== 0 ) { - return OCPPConstants.OCPP_SET_CHARGING_PROFILE_RESPONSE_REJECTED; + return OCPP16Constants.OCPP_SET_CHARGING_PROFILE_RESPONSE_REJECTED } if ( - commandPayload.csChargingProfiles.chargingProfilePurpose === - OCPP16ChargingProfilePurposeType.TX_PROFILE && - (commandPayload.connectorId === 0 || - chargingStation.getConnectorStatus(commandPayload.connectorId)?.transactionStarted === - false) + csChargingProfiles.chargingProfilePurpose === OCPP16ChargingProfilePurposeType.TX_PROFILE && + connectorId === 0 ) { logger.error( - `${chargingStation.logPrefix()} Trying to set transaction charging profile(s) on connector ${ - commandPayload.connectorId - } without a started transaction` - ); - return OCPPConstants.OCPP_SET_CHARGING_PROFILE_RESPONSE_REJECTED; + `${chargingStation.logPrefix()} Trying to set transaction charging profile(s) on connector ${connectorId}` + ) + return OCPP16Constants.OCPP_SET_CHARGING_PROFILE_RESPONSE_REJECTED } - OCPP16ServiceUtils.setChargingProfile( - chargingStation, - commandPayload.connectorId, - commandPayload.csChargingProfiles - ); + const connectorStatus = chargingStation.getConnectorStatus(connectorId) + if ( + csChargingProfiles.chargingProfilePurpose === OCPP16ChargingProfilePurposeType.TX_PROFILE && + connectorId > 0 && + connectorStatus?.transactionStarted === false + ) { + logger.error( + `${chargingStation.logPrefix()} Trying to set transaction charging profile(s) on connector ${connectorId} without a started transaction` + ) + return OCPP16Constants.OCPP_SET_CHARGING_PROFILE_RESPONSE_REJECTED + } + if ( + csChargingProfiles.chargingProfilePurpose === OCPP16ChargingProfilePurposeType.TX_PROFILE && + connectorId > 0 && + connectorStatus?.transactionStarted === true && + csChargingProfiles.transactionId !== connectorStatus.transactionId + ) { + logger.error( + `${chargingStation.logPrefix()} Trying to set transaction charging profile(s) on connector ${connectorId} with a different transaction id ${ + csChargingProfiles.transactionId + } than the started transaction id ${connectorStatus.transactionId}` + ) + return OCPP16Constants.OCPP_SET_CHARGING_PROFILE_RESPONSE_REJECTED + } + OCPP16ServiceUtils.setChargingProfile(chargingStation, connectorId, csChargingProfiles) logger.debug( - `${chargingStation.logPrefix()} Charging profile(s) set on connector id ${ - commandPayload.connectorId - }: %j`, - commandPayload.csChargingProfiles - ); - return OCPPConstants.OCPP_SET_CHARGING_PROFILE_RESPONSE_ACCEPTED; + `${chargingStation.logPrefix()} Charging profile(s) set on connector id ${connectorId}: %j`, + csChargingProfiles + ) + return OCPP16Constants.OCPP_SET_CHARGING_PROFILE_RESPONSE_ACCEPTED } - private handleRequestClearChargingProfile( + private handleRequestGetCompositeSchedule ( chargingStation: ChargingStation, - commandPayload: ClearChargingProfileRequest - ): ClearChargingProfileResponse { + commandPayload: OCPP16GetCompositeScheduleRequest + ): OCPP16GetCompositeScheduleResponse { if ( - OCPP16ServiceUtils.checkFeatureProfile( + !OCPP16ServiceUtils.checkFeatureProfile( chargingStation, OCPP16SupportedFeatureProfiles.SmartCharging, - OCPP16IncomingRequestCommand.CLEAR_CHARGING_PROFILE - ) === false + OCPP16IncomingRequestCommand.GET_COMPOSITE_SCHEDULE + ) ) { - return OCPPConstants.OCPP_CLEAR_CHARGING_PROFILE_RESPONSE_UNKNOWN; + return OCPP16Constants.OCPP_RESPONSE_REJECTED } - if (chargingStation.connectors.has(commandPayload.connectorId) === false) { + const { connectorId, duration, chargingRateUnit } = commandPayload + if (!chargingStation.hasConnector(connectorId)) { logger.error( - `${chargingStation.logPrefix()} Trying to clear a charging profile(s) to a non existing connector Id ${ - commandPayload.connectorId - }` - ); - return OCPPConstants.OCPP_CLEAR_CHARGING_PROFILE_RESPONSE_UNKNOWN; + `${chargingStation.logPrefix()} Trying to get composite schedule to a non existing connector id ${connectorId}` + ) + return OCPP16Constants.OCPP_RESPONSE_REJECTED } - const connectorStatus = chargingStation.getConnectorStatus(commandPayload.connectorId); + if (connectorId === 0) { + logger.error( + `${chargingStation.logPrefix()} Get composite schedule on connector id ${connectorId} is not yet supported` + ) + return OCPP16Constants.OCPP_RESPONSE_REJECTED + } + if (chargingRateUnit != null) { + logger.warn( + `${chargingStation.logPrefix()} Get composite schedule with a specified rate unit is not yet supported, no conversion will be done` + ) + } + const connectorStatus = chargingStation.getConnectorStatus(connectorId) if ( - !Utils.isNullOrUndefined(commandPayload.connectorId) && - Utils.isNotEmptyArray(connectorStatus?.chargingProfiles) + isEmptyArray(connectorStatus?.chargingProfiles) && + isEmptyArray(chargingStation.getConnectorStatus(0)?.chargingProfiles) ) { - connectorStatus.chargingProfiles = []; + return OCPP16Constants.OCPP_RESPONSE_REJECTED + } + const currentDate = new Date() + const compositeScheduleInterval: Interval = { + start: currentDate, + end: addSeconds(currentDate, duration) + } + // Get charging profiles sorted by connector id then stack level + const chargingProfiles: OCPP16ChargingProfile[] = getConnectorChargingProfiles( + chargingStation, + connectorId + ) + let previousCompositeSchedule: OCPP16ChargingSchedule | undefined + let compositeSchedule: OCPP16ChargingSchedule | undefined + for (const chargingProfile of chargingProfiles) { + if (chargingProfile.chargingSchedule.startSchedule == null) { + logger.debug( + `${chargingStation.logPrefix()} ${moduleName}.handleRequestGetCompositeSchedule: Charging profile id ${ + chargingProfile.chargingProfileId + } has no startSchedule defined. Trying to set it to the connector current transaction start date` + ) + // OCPP specifies that if startSchedule is not defined, it should be relative to start of the connector transaction + chargingProfile.chargingSchedule.startSchedule = connectorStatus?.transactionStart + } + if (!isDate(chargingProfile.chargingSchedule.startSchedule)) { + logger.warn( + `${chargingStation.logPrefix()} ${moduleName}.handleRequestGetCompositeSchedule: Charging profile id ${ + chargingProfile.chargingProfileId + } startSchedule property is not a Date instance. Trying to convert it to a Date instance` + ) + chargingProfile.chargingSchedule.startSchedule = convertToDate( + chargingProfile.chargingSchedule.startSchedule + ) + } + if (chargingProfile.chargingSchedule.duration == null) { + logger.debug( + `${chargingStation.logPrefix()} ${moduleName}.handleRequestGetCompositeSchedule: Charging profile id ${ + chargingProfile.chargingProfileId + } has no duration defined and will be set to the maximum time allowed` + ) + // OCPP specifies that if duration is not defined, it should be infinite + chargingProfile.chargingSchedule.duration = differenceInSeconds( + maxTime, + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion + chargingProfile.chargingSchedule.startSchedule! + ) + } + if ( + !prepareChargingProfileKind( + connectorStatus, + chargingProfile, + compositeScheduleInterval.start, + chargingStation.logPrefix() + ) + ) { + continue + } + if ( + !canProceedChargingProfile( + chargingProfile, + compositeScheduleInterval.start, + chargingStation.logPrefix() + ) + ) { + continue + } + compositeSchedule = OCPP16ServiceUtils.composeChargingSchedules( + previousCompositeSchedule, + chargingProfile.chargingSchedule, + compositeScheduleInterval + ) + previousCompositeSchedule = compositeSchedule + } + if (compositeSchedule != null) { + return { + status: GenericStatus.Accepted, + scheduleStart: compositeSchedule.startSchedule, + connectorId, + chargingSchedule: compositeSchedule + } + } + return OCPP16Constants.OCPP_RESPONSE_REJECTED + } + + private handleRequestClearChargingProfile ( + chargingStation: ChargingStation, + commandPayload: OCPP16ClearChargingProfileRequest + ): OCPP16ClearChargingProfileResponse { + if ( + !OCPP16ServiceUtils.checkFeatureProfile( + chargingStation, + OCPP16SupportedFeatureProfiles.SmartCharging, + OCPP16IncomingRequestCommand.CLEAR_CHARGING_PROFILE + ) + ) { + return OCPP16Constants.OCPP_CLEAR_CHARGING_PROFILE_RESPONSE_UNKNOWN + } + const { connectorId } = commandPayload + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion + if (!chargingStation.hasConnector(connectorId!)) { + logger.error( + `${chargingStation.logPrefix()} Trying to clear a charging profile(s) to a non existing connector id ${connectorId}` + ) + return OCPP16Constants.OCPP_CLEAR_CHARGING_PROFILE_RESPONSE_UNKNOWN + } + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion + const connectorStatus = chargingStation.getConnectorStatus(connectorId!) + if (connectorId != null && isNotEmptyArray(connectorStatus?.chargingProfiles)) { + connectorStatus.chargingProfiles = [] logger.debug( - `${chargingStation.logPrefix()} Charging profile(s) cleared on connector id ${ - commandPayload.connectorId - }` - ); - return OCPPConstants.OCPP_CLEAR_CHARGING_PROFILE_RESPONSE_ACCEPTED; + `${chargingStation.logPrefix()} Charging profile(s) cleared on connector id ${connectorId}` + ) + return OCPP16Constants.OCPP_CLEAR_CHARGING_PROFILE_RESPONSE_ACCEPTED } - if (Utils.isNullOrUndefined(commandPayload.connectorId)) { - let clearedCP = false; - for (const connectorId of chargingStation.connectors.keys()) { - if ( - Utils.isNotEmptyArray(chargingStation.getConnectorStatus(connectorId)?.chargingProfiles) - ) { - chargingStation - .getConnectorStatus(connectorId) - ?.chargingProfiles?.forEach((chargingProfile: OCPP16ChargingProfile, index: number) => { - let clearCurrentCP = false; - if (chargingProfile.chargingProfileId === commandPayload.id) { - clearCurrentCP = true; - } - if ( - !commandPayload.chargingProfilePurpose && - chargingProfile.stackLevel === commandPayload.stackLevel - ) { - clearCurrentCP = true; - } - if ( - !chargingProfile.stackLevel && - chargingProfile.chargingProfilePurpose === commandPayload.chargingProfilePurpose - ) { - clearCurrentCP = true; - } - if ( - chargingProfile.stackLevel === commandPayload.stackLevel && - chargingProfile.chargingProfilePurpose === commandPayload.chargingProfilePurpose - ) { - clearCurrentCP = true; - } - if (clearCurrentCP) { - connectorStatus?.chargingProfiles?.splice(index, 1); - logger.debug( - `${chargingStation.logPrefix()} Matching charging profile(s) cleared: %j`, - chargingProfile - ); - clearedCP = true; - } - }); + if (connectorId == null) { + let clearedCP = false + if (chargingStation.hasEvses) { + for (const evseStatus of chargingStation.evses.values()) { + for (const status of evseStatus.connectors.values()) { + clearedCP = OCPP16ServiceUtils.clearChargingProfiles( + chargingStation, + commandPayload, + status.chargingProfiles + ) + } + } + } else { + for (const id of chargingStation.connectors.keys()) { + clearedCP = OCPP16ServiceUtils.clearChargingProfiles( + chargingStation, + commandPayload, + chargingStation.getConnectorStatus(id)?.chargingProfiles + ) } } if (clearedCP) { - return OCPPConstants.OCPP_CLEAR_CHARGING_PROFILE_RESPONSE_ACCEPTED; + return OCPP16Constants.OCPP_CLEAR_CHARGING_PROFILE_RESPONSE_ACCEPTED } } - return OCPPConstants.OCPP_CLEAR_CHARGING_PROFILE_RESPONSE_UNKNOWN; + return OCPP16Constants.OCPP_CLEAR_CHARGING_PROFILE_RESPONSE_UNKNOWN } - private async handleRequestChangeAvailability( + private async handleRequestChangeAvailability ( chargingStation: ChargingStation, - commandPayload: ChangeAvailabilityRequest - ): Promise { - const connectorId: number = commandPayload.connectorId; - if (chargingStation.connectors.has(connectorId) === false) { + commandPayload: OCPP16ChangeAvailabilityRequest + ): Promise { + const { connectorId, type } = commandPayload + if (!chargingStation.hasConnector(connectorId)) { logger.error( - `${chargingStation.logPrefix()} Trying to change the availability of a non existing connector Id ${connectorId.toString()}` - ); - return OCPPConstants.OCPP_AVAILABILITY_RESPONSE_REJECTED; + `${chargingStation.logPrefix()} Trying to change the availability of a non existing connector id ${connectorId}` + ) + return OCPP16Constants.OCPP_AVAILABILITY_RESPONSE_REJECTED } const chargePointStatus: OCPP16ChargePointStatus = - commandPayload.type === OCPP16AvailabilityType.OPERATIVE + type === OCPP16AvailabilityType.Operative ? OCPP16ChargePointStatus.Available - : OCPP16ChargePointStatus.Unavailable; + : OCPP16ChargePointStatus.Unavailable if (connectorId === 0) { - let response: ChangeAvailabilityResponse = OCPPConstants.OCPP_AVAILABILITY_RESPONSE_ACCEPTED; - for (const id of chargingStation.connectors.keys()) { - if (chargingStation.getConnectorStatus(id)?.transactionStarted === true) { - response = OCPPConstants.OCPP_AVAILABILITY_RESPONSE_SCHEDULED; - } - chargingStation.getConnectorStatus(id).availability = commandPayload.type; - if (response === OCPPConstants.OCPP_AVAILABILITY_RESPONSE_ACCEPTED) { - await chargingStation.ocppRequestService.requestHandler< - OCPP16StatusNotificationRequest, - OCPP16StatusNotificationResponse - >(chargingStation, OCPP16RequestCommand.STATUS_NOTIFICATION, { - connectorId: id, - status: chargePointStatus, - errorCode: OCPP16ChargePointErrorCode.NO_ERROR, - }); - chargingStation.getConnectorStatus(id).status = chargePointStatus; + let response: OCPP16ChangeAvailabilityResponse | undefined + if (chargingStation.hasEvses) { + for (const evseStatus of chargingStation.evses.values()) { + response = await OCPP16ServiceUtils.changeAvailability( + chargingStation, + [...evseStatus.connectors.keys()], + chargePointStatus, + type + ) } + } else { + response = await OCPP16ServiceUtils.changeAvailability( + chargingStation, + [...chargingStation.connectors.keys()], + chargePointStatus, + type + ) } - return response; + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion + return response! } else if ( connectorId > 0 && - (chargingStation.isChargingStationAvailable() === true || - (chargingStation.isChargingStationAvailable() === false && - commandPayload.type === OCPP16AvailabilityType.INOPERATIVE)) + (chargingStation.isChargingStationAvailable() || + (!chargingStation.isChargingStationAvailable() && + type === OCPP16AvailabilityType.Inoperative)) ) { if (chargingStation.getConnectorStatus(connectorId)?.transactionStarted === true) { - chargingStation.getConnectorStatus(connectorId).availability = commandPayload.type; - return OCPPConstants.OCPP_AVAILABILITY_RESPONSE_SCHEDULED; + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion + chargingStation.getConnectorStatus(connectorId)!.availability = type + return OCPP16Constants.OCPP_AVAILABILITY_RESPONSE_SCHEDULED } - chargingStation.getConnectorStatus(connectorId).availability = commandPayload.type; - await chargingStation.ocppRequestService.requestHandler< - OCPP16StatusNotificationRequest, - OCPP16StatusNotificationResponse - >(chargingStation, OCPP16RequestCommand.STATUS_NOTIFICATION, { + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion + chargingStation.getConnectorStatus(connectorId)!.availability = type + await OCPP16ServiceUtils.sendAndSetConnectorStatus( + chargingStation, connectorId, - status: chargePointStatus, - errorCode: OCPP16ChargePointErrorCode.NO_ERROR, - }); - chargingStation.getConnectorStatus(connectorId).status = chargePointStatus; - return OCPPConstants.OCPP_AVAILABILITY_RESPONSE_ACCEPTED; + chargePointStatus + ) + return OCPP16Constants.OCPP_AVAILABILITY_RESPONSE_ACCEPTED } - return OCPPConstants.OCPP_AVAILABILITY_RESPONSE_REJECTED; + return OCPP16Constants.OCPP_AVAILABILITY_RESPONSE_REJECTED } - private async handleRequestRemoteStartTransaction( + private async handleRequestRemoteStartTransaction ( chargingStation: ChargingStation, commandPayload: RemoteStartTransactionRequest ): Promise { - const transactionConnectorId = commandPayload.connectorId; - if (chargingStation.connectors.has(transactionConnectorId) === true) { - const remoteStartTransactionLogMsg = `${chargingStation.logPrefix()} Transaction remotely STARTED on ${ - chargingStation.stationInfo.chargingStationId - }#${transactionConnectorId.toString()} for idTag '${commandPayload.idTag}'`; - await chargingStation.ocppRequestService.requestHandler< - OCPP16StatusNotificationRequest, - OCPP16StatusNotificationResponse - >(chargingStation, OCPP16RequestCommand.STATUS_NOTIFICATION, { - connectorId: transactionConnectorId, - status: OCPP16ChargePointStatus.Preparing, - errorCode: OCPP16ChargePointErrorCode.NO_ERROR, - }); - const connectorStatus = chargingStation.getConnectorStatus(transactionConnectorId); - connectorStatus.status = OCPP16ChargePointStatus.Preparing; - if (chargingStation.isChargingStationAvailable() === true) { - // Check if authorized - if (chargingStation.getAuthorizeRemoteTxRequests() === true) { - let authorized = false; - if ( - chargingStation.getLocalAuthListEnabled() === true && - chargingStation.hasAuthorizedTags() === true && - Utils.isNotEmptyString( - chargingStation.authorizedTagsCache - .getAuthorizedTags( - ChargingStationUtils.getAuthorizationFile(chargingStation.stationInfo) - ) - ?.find((idTag) => idTag === commandPayload.idTag) - ) - ) { - connectorStatus.localAuthorizeIdTag = commandPayload.idTag; - connectorStatus.idTagLocalAuthorized = true; - authorized = true; - } else if (chargingStation.getMustAuthorizeAtRemoteStart() === true) { - connectorStatus.authorizeIdTag = commandPayload.idTag; - const authorizeResponse: OCPP16AuthorizeResponse = - await chargingStation.ocppRequestService.requestHandler< - OCPP16AuthorizeRequest, - OCPP16AuthorizeResponse - >(chargingStation, OCPP16RequestCommand.AUTHORIZE, { - idTag: commandPayload.idTag, - }); - if (authorizeResponse?.idTagInfo?.status === OCPP16AuthorizationStatus.ACCEPTED) { - authorized = true; - } - } else { - logger.warn( - `${chargingStation.logPrefix()} The charging station configuration expects authorize at remote start transaction but local authorization or authorize isn't enabled` - ); - } - if (authorized === true) { - // Authorization successful, start transaction - if ( - this.setRemoteStartTransactionChargingProfile( - chargingStation, - transactionConnectorId, - commandPayload.chargingProfile - ) === true - ) { - connectorStatus.transactionRemoteStarted = true; - if ( - ( - await chargingStation.ocppRequestService.requestHandler< - OCPP16StartTransactionRequest, - OCPP16StartTransactionResponse - >(chargingStation, OCPP16RequestCommand.START_TRANSACTION, { - connectorId: transactionConnectorId, - idTag: commandPayload.idTag, - }) - ).idTagInfo.status === OCPP16AuthorizationStatus.ACCEPTED - ) { - logger.debug(remoteStartTransactionLogMsg); - return OCPPConstants.OCPP_RESPONSE_ACCEPTED; - } - return this.notifyRemoteStartTransactionRejected( - chargingStation, - transactionConnectorId, - commandPayload.idTag - ); - } - return this.notifyRemoteStartTransactionRejected( - chargingStation, - transactionConnectorId, - commandPayload.idTag - ); - } - return this.notifyRemoteStartTransactionRejected( - chargingStation, - transactionConnectorId, - commandPayload.idTag - ); - } - // No authorization check required, start transaction - if ( - this.setRemoteStartTransactionChargingProfile( - chargingStation, - transactionConnectorId, - commandPayload.chargingProfile - ) === true - ) { - connectorStatus.transactionRemoteStarted = true; - if ( - ( - await chargingStation.ocppRequestService.requestHandler< - OCPP16StartTransactionRequest, - OCPP16StartTransactionResponse - >(chargingStation, OCPP16RequestCommand.START_TRANSACTION, { - connectorId: transactionConnectorId, - idTag: commandPayload.idTag, - }) - ).idTagInfo.status === OCPP16AuthorizationStatus.ACCEPTED - ) { - logger.debug(remoteStartTransactionLogMsg); - return OCPPConstants.OCPP_RESPONSE_ACCEPTED; - } - return this.notifyRemoteStartTransactionRejected( - chargingStation, - transactionConnectorId, - commandPayload.idTag - ); - } - return this.notifyRemoteStartTransactionRejected( - chargingStation, - transactionConnectorId, - commandPayload.idTag - ); - } - return this.notifyRemoteStartTransactionRejected( + const { connectorId: transactionConnectorId, idTag, chargingProfile } = commandPayload + if (!chargingStation.hasConnector(transactionConnectorId)) { + return await this.notifyRemoteStartTransactionRejected( + chargingStation, + transactionConnectorId, + idTag + ) + } + if ( + !chargingStation.isChargingStationAvailable() || + !chargingStation.isConnectorAvailable(transactionConnectorId) + ) { + return await this.notifyRemoteStartTransactionRejected( + chargingStation, + transactionConnectorId, + idTag + ) + } + // idTag authorization check required + if ( + chargingStation.getAuthorizeRemoteTxRequests() && + !(await OCPP16ServiceUtils.isIdTagAuthorized(chargingStation, transactionConnectorId, idTag)) + ) { + return await this.notifyRemoteStartTransactionRejected( chargingStation, transactionConnectorId, - commandPayload.idTag - ); + idTag + ) } - return this.notifyRemoteStartTransactionRejected( + await OCPP16ServiceUtils.sendAndSetConnectorStatus( chargingStation, transactionConnectorId, - commandPayload.idTag - ); + OCPP16ChargePointStatus.Preparing + ) + if ( + chargingProfile != null && + !this.setRemoteStartTransactionChargingProfile( + chargingStation, + transactionConnectorId, + chargingProfile + ) + ) { + return await this.notifyRemoteStartTransactionRejected( + chargingStation, + transactionConnectorId, + idTag + ) + } + logger.debug( + `${chargingStation.logPrefix()} Remote start transaction ACCEPTED on connector id ${transactionConnectorId}, idTag '${idTag}'` + ) + return OCPP16Constants.OCPP_RESPONSE_ACCEPTED } - private async notifyRemoteStartTransactionRejected( + private async notifyRemoteStartTransactionRejected ( chargingStation: ChargingStation, connectorId: number, idTag: string ): Promise { - if ( - chargingStation.getConnectorStatus(connectorId)?.status !== OCPP16ChargePointStatus.Available - ) { - await chargingStation.ocppRequestService.requestHandler< - OCPP16StatusNotificationRequest, - OCPP16StatusNotificationResponse - >(chargingStation, OCPP16RequestCommand.STATUS_NOTIFICATION, { + const connectorStatus = chargingStation.getConnectorStatus(connectorId) + if (connectorStatus?.status !== OCPP16ChargePointStatus.Available) { + await OCPP16ServiceUtils.sendAndSetConnectorStatus( + chargingStation, connectorId, - status: OCPP16ChargePointStatus.Available, - errorCode: OCPP16ChargePointErrorCode.NO_ERROR, - }); - chargingStation.getConnectorStatus(connectorId).status = OCPP16ChargePointStatus.Available; + OCPP16ChargePointStatus.Available + ) } - logger.warn( - `${chargingStation.logPrefix()} Remote starting transaction REJECTED on connector Id ${connectorId.toString()}, idTag '${idTag}', availability '${ - chargingStation.getConnectorStatus(connectorId)?.availability - }', status '${chargingStation.getConnectorStatus(connectorId)?.status}'` - ); - return OCPPConstants.OCPP_RESPONSE_REJECTED; + logger.debug( + `${chargingStation.logPrefix()} Remote start transaction REJECTED on connector id ${connectorId}, idTag '${idTag}', availability '${connectorStatus?.availability}', status '${connectorStatus?.status}'` + ) + return OCPP16Constants.OCPP_RESPONSE_REJECTED } - private setRemoteStartTransactionChargingProfile( + private setRemoteStartTransactionChargingProfile ( chargingStation: ChargingStation, connectorId: number, - cp: OCPP16ChargingProfile + chargingProfile: OCPP16ChargingProfile ): boolean { - if (cp && cp.chargingProfilePurpose === OCPP16ChargingProfilePurposeType.TX_PROFILE) { - OCPP16ServiceUtils.setChargingProfile(chargingStation, connectorId, cp); + if (chargingProfile.chargingProfilePurpose === OCPP16ChargingProfilePurposeType.TX_PROFILE) { + OCPP16ServiceUtils.setChargingProfile(chargingStation, connectorId, chargingProfile) logger.debug( `${chargingStation.logPrefix()} Charging profile(s) set at remote start transaction on connector id ${connectorId}: %j`, - cp - ); - return true; - } else if (cp && cp.chargingProfilePurpose !== OCPP16ChargingProfilePurposeType.TX_PROFILE) { - logger.warn( - `${chargingStation.logPrefix()} Not allowed to set ${ - cp.chargingProfilePurpose - } charging profile(s) at remote start transaction` - ); - return false; - } else if (!cp) { - return true; + chargingProfile + ) + return true } + logger.debug( + `${chargingStation.logPrefix()} Not allowed to set ${ + chargingProfile.chargingProfilePurpose + } charging profile(s) at remote start transaction` + ) + return false } - private async handleRequestRemoteStopTransaction( + private handleRequestRemoteStopTransaction ( chargingStation: ChargingStation, commandPayload: RemoteStopTransactionRequest - ): Promise { - const transactionId = commandPayload.transactionId; - for (const connectorId of chargingStation.connectors.keys()) { - if ( - connectorId > 0 && - chargingStation.getConnectorStatus(connectorId)?.transactionId === transactionId - ) { - await chargingStation.ocppRequestService.requestHandler< - OCPP16StatusNotificationRequest, - OCPP16StatusNotificationResponse - >(chargingStation, OCPP16RequestCommand.STATUS_NOTIFICATION, { - connectorId, - status: OCPP16ChargePointStatus.Finishing, - errorCode: OCPP16ChargePointErrorCode.NO_ERROR, - }); - chargingStation.getConnectorStatus(connectorId).status = OCPP16ChargePointStatus.Finishing; - const stopResponse = await chargingStation.stopTransactionOnConnector( - connectorId, - OCPP16StopTransactionReason.REMOTE - ); - if (stopResponse.idTagInfo?.status === OCPP16AuthorizationStatus.ACCEPTED) { - return OCPPConstants.OCPP_RESPONSE_ACCEPTED; - } - return OCPPConstants.OCPP_RESPONSE_REJECTED; - } + ): GenericResponse { + const { transactionId } = commandPayload + if (chargingStation.getConnectorIdByTransactionId(transactionId) != null) { + logger.debug( + `${chargingStation.logPrefix()} Remote stop transaction ACCEPTED for transactionId '${transactionId}'` + ) + return OCPP16Constants.OCPP_RESPONSE_ACCEPTED } - logger.warn( - `${chargingStation.logPrefix()} Trying to remote stop a non existing transaction ${transactionId.toString()}` - ); - return OCPPConstants.OCPP_RESPONSE_REJECTED; + logger.debug( + `${chargingStation.logPrefix()} Remote stop transaction REJECTED for transactionId '${transactionId}'` + ) + return OCPP16Constants.OCPP_RESPONSE_REJECTED } - private handleRequestUpdateFirmware( + private handleRequestUpdateFirmware ( chargingStation: ChargingStation, commandPayload: OCPP16UpdateFirmwareRequest ): OCPP16UpdateFirmwareResponse { if ( - OCPP16ServiceUtils.checkFeatureProfile( + !OCPP16ServiceUtils.checkFeatureProfile( chargingStation, OCPP16SupportedFeatureProfiles.FirmwareManagement, OCPP16IncomingRequestCommand.UPDATE_FIRMWARE - ) === false + ) ) { logger.warn( `${chargingStation.logPrefix()} ${moduleName}.handleRequestUpdateFirmware: Cannot simulate firmware update: feature profile not supported` - ); - return OCPPConstants.OCPP_RESPONSE_EMPTY; + ) + return OCPP16Constants.OCPP_RESPONSE_EMPTY } - if ( - !Utils.isNullOrUndefined(chargingStation.stationInfo.firmwareStatus) && - chargingStation.stationInfo.firmwareStatus !== OCPP16FirmwareStatus.Installed - ) { + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion + commandPayload.retrieveDate = convertToDate(commandPayload.retrieveDate)! + const { retrieveDate } = commandPayload + if (chargingStation.stationInfo?.firmwareStatus !== OCPP16FirmwareStatus.Installed) { logger.warn( `${chargingStation.logPrefix()} ${moduleName}.handleRequestUpdateFirmware: Cannot simulate firmware update: firmware update is already in progress` - ); - return OCPPConstants.OCPP_RESPONSE_EMPTY; + ) + return OCPP16Constants.OCPP_RESPONSE_EMPTY } - const retrieveDate = Utils.convertToDate(commandPayload.retrieveDate); - const now = Date.now(); - if (retrieveDate?.getTime() <= now) { - this.runInAsyncScope( - this.updateFirmware.bind(this) as ( - this: OCPP16IncomingRequestService, - ...args: any[] - ) => Promise, - this, - chargingStation - ).catch(Constants.EMPTY_FUNCTION); + const now = Date.now() + if (retrieveDate.getTime() <= now) { + this.updateFirmwareSimulation(chargingStation).catch(Constants.EMPTY_FUNCTION) } else { setTimeout(() => { - this.updateFirmware(chargingStation).catch(Constants.EMPTY_FUNCTION); - }, retrieveDate?.getTime() - now); + this.updateFirmwareSimulation(chargingStation).catch(Constants.EMPTY_FUNCTION) + }, retrieveDate.getTime() - now) } - return OCPPConstants.OCPP_RESPONSE_EMPTY; + return OCPP16Constants.OCPP_RESPONSE_EMPTY } - private async updateFirmware( + private async updateFirmwareSimulation ( chargingStation: ChargingStation, maxDelay = 30, minDelay = 15 ): Promise { - chargingStation.stopAutomaticTransactionGenerator(); - for (const connectorId of chargingStation.connectors.keys()) { - if ( - connectorId > 0 && - chargingStation.getConnectorStatus(connectorId)?.transactionStarted === false - ) { - await chargingStation.ocppRequestService.requestHandler< - OCPP16StatusNotificationRequest, - OCPP16StatusNotificationResponse - >(chargingStation, OCPP16RequestCommand.STATUS_NOTIFICATION, { - connectorId, - status: OCPP16ChargePointStatus.Unavailable, - errorCode: OCPP16ChargePointErrorCode.NO_ERROR, - }); - chargingStation.getConnectorStatus(connectorId).status = - OCPP16ChargePointStatus.Unavailable; + if (!checkChargingStation(chargingStation, chargingStation.logPrefix())) { + return + } + if (chargingStation.hasEvses) { + for (const [evseId, evseStatus] of chargingStation.evses) { + if (evseId > 0) { + for (const [connectorId, connectorStatus] of evseStatus.connectors) { + if (connectorStatus.transactionStarted === false) { + await OCPP16ServiceUtils.sendAndSetConnectorStatus( + chargingStation, + connectorId, + OCPP16ChargePointStatus.Unavailable + ) + } + } + } + } + } else { + for (const connectorId of chargingStation.connectors.keys()) { + if ( + connectorId > 0 && + chargingStation.getConnectorStatus(connectorId)?.transactionStarted === false + ) { + await OCPP16ServiceUtils.sendAndSetConnectorStatus( + chargingStation, + connectorId, + OCPP16ChargePointStatus.Unavailable + ) + } } } + await chargingStation.ocppRequestService.requestHandler< + OCPP16FirmwareStatusNotificationRequest, + OCPP16FirmwareStatusNotificationResponse + >(chargingStation, OCPP16RequestCommand.FIRMWARE_STATUS_NOTIFICATION, { + status: OCPP16FirmwareStatus.Downloading + }) + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion + chargingStation.stationInfo!.firmwareStatus = OCPP16FirmwareStatus.Downloading if ( - chargingStation.stationInfo?.firmwareUpgrade?.failureStatus && - Utils.isNotEmptyString(chargingStation.stationInfo?.firmwareUpgrade?.failureStatus) + chargingStation.stationInfo?.firmwareUpgrade?.failureStatus === + OCPP16FirmwareStatus.DownloadFailed ) { + await sleep(secondsToMilliseconds(getRandomInteger(maxDelay, minDelay))) await chargingStation.ocppRequestService.requestHandler< - OCPP16FirmwareStatusNotificationRequest, - OCPP16FirmwareStatusNotificationResponse + OCPP16FirmwareStatusNotificationRequest, + OCPP16FirmwareStatusNotificationResponse >(chargingStation, OCPP16RequestCommand.FIRMWARE_STATUS_NOTIFICATION, { - status: chargingStation.stationInfo?.firmwareUpgrade?.failureStatus, - }); - return; + status: chargingStation.stationInfo.firmwareUpgrade.failureStatus + }) + chargingStation.stationInfo.firmwareStatus = + chargingStation.stationInfo.firmwareUpgrade.failureStatus + return } + await sleep(secondsToMilliseconds(getRandomInteger(maxDelay, minDelay))) await chargingStation.ocppRequestService.requestHandler< - OCPP16FirmwareStatusNotificationRequest, - OCPP16FirmwareStatusNotificationResponse + OCPP16FirmwareStatusNotificationRequest, + OCPP16FirmwareStatusNotificationResponse >(chargingStation, OCPP16RequestCommand.FIRMWARE_STATUS_NOTIFICATION, { - status: OCPP16FirmwareStatus.Downloading, - }); - chargingStation.stationInfo.firmwareStatus = OCPP16FirmwareStatus.Downloading; - await Utils.sleep(Utils.getRandomInteger(maxDelay, minDelay) * 1000); + status: OCPP16FirmwareStatus.Downloaded + }) + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion + chargingStation.stationInfo!.firmwareStatus = OCPP16FirmwareStatus.Downloaded + let wasTransactionsStarted = false + let transactionsStarted: boolean + do { + const runningTransactions = chargingStation.getNumberOfRunningTransactions() + if (runningTransactions > 0) { + const waitTime = secondsToMilliseconds(15) + logger.debug( + `${chargingStation.logPrefix()} ${moduleName}.updateFirmwareSimulation: ${runningTransactions} transaction(s) in progress, waiting ${formatDurationMilliSeconds( + waitTime + )} before continuing firmware update simulation` + ) + await sleep(waitTime) + transactionsStarted = true + wasTransactionsStarted = true + } else { + if (chargingStation.hasEvses) { + for (const [evseId, evseStatus] of chargingStation.evses) { + if (evseId > 0) { + for (const [connectorId, connectorStatus] of evseStatus.connectors) { + if (connectorStatus.status !== OCPP16ChargePointStatus.Unavailable) { + await OCPP16ServiceUtils.sendAndSetConnectorStatus( + chargingStation, + connectorId, + OCPP16ChargePointStatus.Unavailable + ) + } + } + } + } + } else { + for (const connectorId of chargingStation.connectors.keys()) { + if ( + connectorId > 0 && + chargingStation.getConnectorStatus(connectorId)?.status !== + OCPP16ChargePointStatus.Unavailable + ) { + await OCPP16ServiceUtils.sendAndSetConnectorStatus( + chargingStation, + connectorId, + OCPP16ChargePointStatus.Unavailable + ) + } + } + } + transactionsStarted = false + } + } while (transactionsStarted) + !wasTransactionsStarted && + (await sleep(secondsToMilliseconds(getRandomInteger(maxDelay, minDelay)))) + if (!checkChargingStation(chargingStation, chargingStation.logPrefix())) { + return + } await chargingStation.ocppRequestService.requestHandler< - OCPP16FirmwareStatusNotificationRequest, - OCPP16FirmwareStatusNotificationResponse + OCPP16FirmwareStatusNotificationRequest, + OCPP16FirmwareStatusNotificationResponse >(chargingStation, OCPP16RequestCommand.FIRMWARE_STATUS_NOTIFICATION, { - status: OCPP16FirmwareStatus.Downloaded, - }); - chargingStation.stationInfo.firmwareStatus = OCPP16FirmwareStatus.Downloaded; - await Utils.sleep(Utils.getRandomInteger(maxDelay, minDelay) * 1000); - await chargingStation.ocppRequestService.requestHandler< + status: OCPP16FirmwareStatus.Installing + }) + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion + chargingStation.stationInfo!.firmwareStatus = OCPP16FirmwareStatus.Installing + if ( + chargingStation.stationInfo?.firmwareUpgrade?.failureStatus === + OCPP16FirmwareStatus.InstallationFailed + ) { + await sleep(secondsToMilliseconds(getRandomInteger(maxDelay, minDelay))) + await chargingStation.ocppRequestService.requestHandler< OCPP16FirmwareStatusNotificationRequest, OCPP16FirmwareStatusNotificationResponse - >(chargingStation, OCPP16RequestCommand.FIRMWARE_STATUS_NOTIFICATION, { - status: OCPP16FirmwareStatus.Installing, - }); - chargingStation.stationInfo.firmwareStatus = OCPP16FirmwareStatus.Installing; + >(chargingStation, OCPP16RequestCommand.FIRMWARE_STATUS_NOTIFICATION, { + status: chargingStation.stationInfo.firmwareUpgrade.failureStatus + }) + chargingStation.stationInfo.firmwareStatus = + chargingStation.stationInfo.firmwareUpgrade.failureStatus + return + } if (chargingStation.stationInfo?.firmwareUpgrade?.reset === true) { - await Utils.sleep(Utils.getRandomInteger(maxDelay, minDelay) * 1000); - await chargingStation.reset(OCPP16StopTransactionReason.REBOOT); + await sleep(secondsToMilliseconds(getRandomInteger(maxDelay, minDelay))) + await chargingStation.reset(OCPP16StopTransactionReason.REBOOT) } } - private async handleRequestGetDiagnostics( + private async handleRequestGetDiagnostics ( chargingStation: ChargingStation, commandPayload: GetDiagnosticsRequest ): Promise { if ( - OCPP16ServiceUtils.checkFeatureProfile( + !OCPP16ServiceUtils.checkFeatureProfile( chargingStation, OCPP16SupportedFeatureProfiles.FirmwareManagement, OCPP16IncomingRequestCommand.GET_DIAGNOSTICS - ) === false + ) ) { logger.warn( `${chargingStation.logPrefix()} ${moduleName}.handleRequestGetDiagnostics: Cannot get diagnostics: feature profile not supported` - ); - return OCPPConstants.OCPP_RESPONSE_EMPTY; + ) + return OCPP16Constants.OCPP_RESPONSE_EMPTY } - const uri = new URL(commandPayload.location); + const { location } = commandPayload + const uri = new URL(location) if (uri.protocol.startsWith('ftp:')) { - let ftpClient: Client; + let ftpClient: Client | undefined try { - const logFiles = fs - .readdirSync(path.resolve(path.dirname(fileURLToPath(import.meta.url)), '../../../../')) - .filter((file) => file.endsWith('.log')) - .map((file) => path.join('./', file)); - const diagnosticsArchive = `${chargingStation.stationInfo.chargingStationId}_logs.tar.gz`; - tar.create({ gzip: true }, logFiles).pipe(fs.createWriteStream(diagnosticsArchive)); - ftpClient = new Client(); + const logFiles = readdirSync(resolve(dirname(fileURLToPath(import.meta.url)), '../')) + .filter(file => file.endsWith('.log')) + .map(file => join('./', file)) + const diagnosticsArchive = `${chargingStation.stationInfo?.chargingStationId}_logs.tar.gz` + create({ gzip: true }, logFiles).pipe(createWriteStream(diagnosticsArchive)) + ftpClient = new Client() const accessResponse = await ftpClient.access({ host: uri.host, - ...(Utils.isNotEmptyString(uri.port) && { port: Utils.convertToInt(uri.port) }), - ...(Utils.isNotEmptyString(uri.username) && { user: uri.username }), - ...(Utils.isNotEmptyString(uri.password) && { password: uri.password }), - }); - let uploadResponse: FTPResponse; + ...(isNotEmptyString(uri.port) && { port: convertToInt(uri.port) }), + ...(isNotEmptyString(uri.username) && { user: uri.username }), + ...(isNotEmptyString(uri.password) && { password: uri.password }) + }) + let uploadResponse: FTPResponse | undefined if (accessResponse.code === 220) { - ftpClient.trackProgress((info) => { + ftpClient.trackProgress(info => { logger.info( - `${chargingStation.logPrefix()} ${ + `${chargingStation.logPrefix()} ${moduleName}.handleRequestGetDiagnostics: ${ info.bytes / 1024 } bytes transferred from diagnostics archive ${info.name}` - ); + ) chargingStation.ocppRequestService .requestHandler< - OCPP16DiagnosticsStatusNotificationRequest, - OCPP16DiagnosticsStatusNotificationResponse - >(chargingStation, OCPP16RequestCommand.DIAGNOSTICS_STATUS_NOTIFICATION, { - status: OCPP16DiagnosticsStatus.Uploading, - }) - .catch((error) => { + OCPP16DiagnosticsStatusNotificationRequest, + OCPP16DiagnosticsStatusNotificationResponse + >(chargingStation, OCPP16RequestCommand.DIAGNOSTICS_STATUS_NOTIFICATION, { + status: OCPP16DiagnosticsStatus.Uploading + }) + .catch(error => { logger.error( `${chargingStation.logPrefix()} ${moduleName}.handleRequestGetDiagnostics: Error while sending '${ OCPP16RequestCommand.DIAGNOSTICS_STATUS_NOTIFICATION }'`, error - ); - }); - }); + ) + }) + }) uploadResponse = await ftpClient.uploadFrom( - path.join( - path.resolve(path.dirname(fileURLToPath(import.meta.url)), '../../../../'), - diagnosticsArchive - ), + join(resolve(dirname(fileURLToPath(import.meta.url)), '../'), diagnosticsArchive), `${uri.pathname}${diagnosticsArchive}` - ); + ) if (uploadResponse.code === 226) { await chargingStation.ocppRequestService.requestHandler< - OCPP16DiagnosticsStatusNotificationRequest, - OCPP16DiagnosticsStatusNotificationResponse + OCPP16DiagnosticsStatusNotificationRequest, + OCPP16DiagnosticsStatusNotificationResponse >(chargingStation, OCPP16RequestCommand.DIAGNOSTICS_STATUS_NOTIFICATION, { - status: OCPP16DiagnosticsStatus.Uploaded, - }); - if (ftpClient) { - ftpClient.close(); - } - return { fileName: diagnosticsArchive }; + status: OCPP16DiagnosticsStatus.Uploaded + }) + ftpClient.close() + return { fileName: diagnosticsArchive } } throw new OCPPError( ErrorType.GENERIC_ERROR, - `Diagnostics transfer failed with error code ${accessResponse.code.toString()}${ - uploadResponse?.code && `|${uploadResponse?.code.toString()}` - }`, + `Diagnostics transfer failed with error code ${accessResponse.code}|${uploadResponse.code}`, OCPP16IncomingRequestCommand.GET_DIAGNOSTICS - ); + ) } throw new OCPPError( ErrorType.GENERIC_ERROR, - `Diagnostics transfer failed with error code ${accessResponse.code.toString()}${ - uploadResponse?.code && `|${uploadResponse?.code.toString()}` - }`, + `Diagnostics transfer failed with error code ${accessResponse.code}|${uploadResponse?.code}`, OCPP16IncomingRequestCommand.GET_DIAGNOSTICS - ); + ) } catch (error) { await chargingStation.ocppRequestService.requestHandler< - OCPP16DiagnosticsStatusNotificationRequest, - OCPP16DiagnosticsStatusNotificationResponse + OCPP16DiagnosticsStatusNotificationRequest, + OCPP16DiagnosticsStatusNotificationResponse >(chargingStation, OCPP16RequestCommand.DIAGNOSTICS_STATUS_NOTIFICATION, { - status: OCPP16DiagnosticsStatus.UploadFailed, - }); - if (ftpClient) { - ftpClient.close(); - } - return this.handleIncomingRequestError( + status: OCPP16DiagnosticsStatus.UploadFailed + }) + ftpClient?.close() + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion + return this.handleIncomingRequestError( chargingStation, OCPP16IncomingRequestCommand.GET_DIAGNOSTICS, error as Error, - { errorResponse: OCPPConstants.OCPP_RESPONSE_EMPTY } - ); + { errorResponse: OCPP16Constants.OCPP_RESPONSE_EMPTY } + )! } } else { logger.error( `${chargingStation.logPrefix()} Unsupported protocol ${ uri.protocol } to transfer the diagnostic logs archive` - ); + ) await chargingStation.ocppRequestService.requestHandler< - OCPP16DiagnosticsStatusNotificationRequest, - OCPP16DiagnosticsStatusNotificationResponse + OCPP16DiagnosticsStatusNotificationRequest, + OCPP16DiagnosticsStatusNotificationResponse >(chargingStation, OCPP16RequestCommand.DIAGNOSTICS_STATUS_NOTIFICATION, { - status: OCPP16DiagnosticsStatus.UploadFailed, - }); - return OCPPConstants.OCPP_RESPONSE_EMPTY; + status: OCPP16DiagnosticsStatus.UploadFailed + }) + return OCPP16Constants.OCPP_RESPONSE_EMPTY } } - private handleRequestTriggerMessage( + private handleRequestTriggerMessage ( chargingStation: ChargingStation, commandPayload: OCPP16TriggerMessageRequest ): OCPP16TriggerMessageResponse { + const { requestedMessage, connectorId } = commandPayload if ( !OCPP16ServiceUtils.checkFeatureProfile( chargingStation, OCPP16SupportedFeatureProfiles.RemoteTrigger, OCPP16IncomingRequestCommand.TRIGGER_MESSAGE ) || - !OCPP16ServiceUtils.isMessageTriggerSupported( - chargingStation, - commandPayload.requestedMessage - ) + !OCPP16ServiceUtils.isMessageTriggerSupported(chargingStation, requestedMessage) ) { - return OCPPConstants.OCPP_TRIGGER_MESSAGE_RESPONSE_NOT_IMPLEMENTED; + return OCPP16Constants.OCPP_TRIGGER_MESSAGE_RESPONSE_NOT_IMPLEMENTED } if ( !OCPP16ServiceUtils.isConnectorIdValid( chargingStation, OCPP16IncomingRequestCommand.TRIGGER_MESSAGE, - commandPayload.connectorId + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion + connectorId! ) ) { - return OCPPConstants.OCPP_TRIGGER_MESSAGE_RESPONSE_REJECTED; + return OCPP16Constants.OCPP_TRIGGER_MESSAGE_RESPONSE_REJECTED + } + switch (requestedMessage) { + case OCPP16MessageTrigger.BootNotification: + case OCPP16MessageTrigger.Heartbeat: + case OCPP16MessageTrigger.StatusNotification: + return OCPP16Constants.OCPP_TRIGGER_MESSAGE_RESPONSE_ACCEPTED + default: + return OCPP16Constants.OCPP_TRIGGER_MESSAGE_RESPONSE_NOT_IMPLEMENTED } + } + + private handleRequestDataTransfer ( + chargingStation: ChargingStation, + commandPayload: OCPP16DataTransferRequest + ): OCPP16DataTransferResponse { + const { vendorId } = commandPayload try { - switch (commandPayload.requestedMessage) { - case OCPP16MessageTrigger.BootNotification: - setTimeout(() => { - chargingStation.ocppRequestService - .requestHandler( - chargingStation, - OCPP16RequestCommand.BOOT_NOTIFICATION, - chargingStation.bootNotificationRequest, - { skipBufferingOnError: true, triggerMessage: true } - ) - .then((response) => { - chargingStation.bootNotificationResponse = response; - }) - .catch(Constants.EMPTY_FUNCTION); - }, Constants.OCPP_TRIGGER_MESSAGE_DELAY); - return OCPPConstants.OCPP_TRIGGER_MESSAGE_RESPONSE_ACCEPTED; - case OCPP16MessageTrigger.Heartbeat: - setTimeout(() => { - chargingStation.ocppRequestService - .requestHandler( - chargingStation, - OCPP16RequestCommand.HEARTBEAT, - null, - { - triggerMessage: true, - } - ) - .catch(Constants.EMPTY_FUNCTION); - }, Constants.OCPP_TRIGGER_MESSAGE_DELAY); - return OCPPConstants.OCPP_TRIGGER_MESSAGE_RESPONSE_ACCEPTED; - case OCPP16MessageTrigger.StatusNotification: - setTimeout(() => { - if (!Utils.isNullOrUndefined(commandPayload?.connectorId)) { - chargingStation.ocppRequestService - .requestHandler( - chargingStation, - OCPP16RequestCommand.STATUS_NOTIFICATION, - { - connectorId: commandPayload.connectorId, - errorCode: OCPP16ChargePointErrorCode.NO_ERROR, - status: chargingStation.getConnectorStatus(commandPayload.connectorId)?.status, - }, - { - triggerMessage: true, - } - ) - .catch(Constants.EMPTY_FUNCTION); - } else { - for (const connectorId of chargingStation.connectors.keys()) { - chargingStation.ocppRequestService - .requestHandler< - OCPP16StatusNotificationRequest, - OCPP16StatusNotificationResponse - >( - chargingStation, - OCPP16RequestCommand.STATUS_NOTIFICATION, - { - connectorId, - errorCode: OCPP16ChargePointErrorCode.NO_ERROR, - status: chargingStation.getConnectorStatus(connectorId)?.status, - }, - { - triggerMessage: true, - } - ) - .catch(Constants.EMPTY_FUNCTION); - } - } - }, Constants.OCPP_TRIGGER_MESSAGE_DELAY); - return OCPPConstants.OCPP_TRIGGER_MESSAGE_RESPONSE_ACCEPTED; + if (Object.values(OCPP16DataTransferVendorId).includes(vendorId)) { + return OCPP16Constants.OCPP_DATA_TRANSFER_RESPONSE_ACCEPTED + } + return OCPP16Constants.OCPP_DATA_TRANSFER_RESPONSE_UNKNOWN_VENDOR_ID + } catch (error) { + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion + return this.handleIncomingRequestError( + chargingStation, + OCPP16IncomingRequestCommand.DATA_TRANSFER, + error as Error, + { errorResponse: OCPP16Constants.OCPP_DATA_TRANSFER_RESPONSE_REJECTED } + )! + } + } + + private async handleRequestReserveNow ( + chargingStation: ChargingStation, + commandPayload: OCPP16ReserveNowRequest + ): Promise { + if ( + !OCPP16ServiceUtils.checkFeatureProfile( + chargingStation, + OCPP16SupportedFeatureProfiles.Reservation, + OCPP16IncomingRequestCommand.RESERVE_NOW + ) + ) { + return OCPP16Constants.OCPP_RESERVATION_RESPONSE_REJECTED + } + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion + commandPayload.expiryDate = convertToDate(commandPayload.expiryDate)! + const { reservationId, idTag, connectorId } = commandPayload + let response: OCPP16ReserveNowResponse + try { + if (connectorId > 0 && !chargingStation.isConnectorAvailable(connectorId)) { + return OCPP16Constants.OCPP_RESERVATION_RESPONSE_REJECTED + } + if (connectorId === 0 && !chargingStation.getReserveConnectorZeroSupported()) { + return OCPP16Constants.OCPP_RESERVATION_RESPONSE_REJECTED + } + if (!(await OCPP16ServiceUtils.isIdTagAuthorized(chargingStation, connectorId, idTag))) { + return OCPP16Constants.OCPP_RESERVATION_RESPONSE_REJECTED + } + await removeExpiredReservations(chargingStation) + switch (chargingStation.getConnectorStatus(connectorId)?.status) { + case OCPP16ChargePointStatus.Faulted: + response = OCPP16Constants.OCPP_RESERVATION_RESPONSE_FAULTED + break + case OCPP16ChargePointStatus.Preparing: + case OCPP16ChargePointStatus.Charging: + case OCPP16ChargePointStatus.SuspendedEV: + case OCPP16ChargePointStatus.SuspendedEVSE: + case OCPP16ChargePointStatus.Finishing: + response = OCPP16Constants.OCPP_RESERVATION_RESPONSE_OCCUPIED + break + case OCPP16ChargePointStatus.Unavailable: + response = OCPP16Constants.OCPP_RESERVATION_RESPONSE_UNAVAILABLE + break + case OCPP16ChargePointStatus.Reserved: + if (!chargingStation.isConnectorReservable(reservationId, idTag, connectorId)) { + response = OCPP16Constants.OCPP_RESERVATION_RESPONSE_OCCUPIED + break + } + // eslint-disable-next-line no-fallthrough default: - return OCPPConstants.OCPP_TRIGGER_MESSAGE_RESPONSE_NOT_IMPLEMENTED; + if (!chargingStation.isConnectorReservable(reservationId, idTag)) { + response = OCPP16Constants.OCPP_RESERVATION_RESPONSE_OCCUPIED + break + } + await chargingStation.addReservation({ + id: commandPayload.reservationId, + ...commandPayload + }) + response = OCPP16Constants.OCPP_RESERVATION_RESPONSE_ACCEPTED + break } + return response } catch (error) { - return this.handleIncomingRequestError( + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion + chargingStation.getConnectorStatus(connectorId)!.status = OCPP16ChargePointStatus.Available + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion + return this.handleIncomingRequestError( chargingStation, - OCPP16IncomingRequestCommand.TRIGGER_MESSAGE, + OCPP16IncomingRequestCommand.RESERVE_NOW, error as Error, - { errorResponse: OCPPConstants.OCPP_TRIGGER_MESSAGE_RESPONSE_REJECTED } - ); + { errorResponse: OCPP16Constants.OCPP_RESERVATION_RESPONSE_FAULTED } + )! } } - private handleRequestDataTransfer( + private async handleRequestCancelReservation ( chargingStation: ChargingStation, - commandPayload: OCPP16DataTransferRequest - ): OCPP16DataTransferResponse { + commandPayload: OCPP16CancelReservationRequest + ): Promise { + if ( + !OCPP16ServiceUtils.checkFeatureProfile( + chargingStation, + OCPP16SupportedFeatureProfiles.Reservation, + OCPP16IncomingRequestCommand.CANCEL_RESERVATION + ) + ) { + return OCPP16Constants.OCPP_CANCEL_RESERVATION_RESPONSE_REJECTED + } try { - if (Object.values(OCPP16DataTransferVendorId).includes(commandPayload.vendorId)) { - return { - status: OCPP16DataTransferStatus.ACCEPTED, - }; + const { reservationId } = commandPayload + const reservation = chargingStation.getReservationBy('reservationId', reservationId) + if (reservation == null) { + logger.debug( + `${chargingStation.logPrefix()} Reservation with id ${reservationId} does not exist on charging station` + ) + return OCPP16Constants.OCPP_CANCEL_RESERVATION_RESPONSE_REJECTED } - return { - status: OCPP16DataTransferStatus.UNKNOWN_VENDOR_ID, - }; + await chargingStation.removeReservation( + reservation, + ReservationTerminationReason.RESERVATION_CANCELED + ) + return OCPP16Constants.OCPP_CANCEL_RESERVATION_RESPONSE_ACCEPTED } catch (error) { - return this.handleIncomingRequestError( + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion + return this.handleIncomingRequestError( chargingStation, - OCPP16IncomingRequestCommand.DATA_TRANSFER, + OCPP16IncomingRequestCommand.CANCEL_RESERVATION, error as Error, - { errorResponse: OCPPConstants.OCPP_DATA_TRANSFER_RESPONSE_REJECTED } - ); + { errorResponse: OCPP16Constants.OCPP_CANCEL_RESERVATION_RESPONSE_REJECTED } + )! } } }