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=ebec27d23544cd40f741b7429042464534fdaf9f;hpb=77b95a89411dcee5df14a283f05f6d6020db08ef;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 ebec27d2..c8bc557c 100644 --- a/src/charging-station/ocpp/1.6/OCPP16IncomingRequestService.ts +++ b/src/charging-station/ocpp/1.6/OCPP16IncomingRequestService.ts @@ -1,305 +1,603 @@ -// Partial Copyright Jerome Benoit. 2021. All Rights Reserved. +// Partial Copyright Jerome Benoit. 2021-2024. All Rights Reserved. -import fs from 'fs'; -import path from 'path'; -import { URL, fileURLToPath } from '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, FTPResponse } from 'basic-ftp'; -import tar from 'tar'; - -import OCPPError from '../../../exception/OCPPError'; -import type { JsonObject, JsonType } from '../../../types/JsonType'; -import { OCPP16ChargePointErrorCode } from '../../../types/ocpp/1.6/ChargePointErrorCode'; -import { OCPP16ChargePointStatus } from '../../../types/ocpp/1.6/ChargePointStatus'; +import type { ValidateFunction } from 'ajv' +import { Client, type FTPResponse } from 'basic-ftp' import { - ChargingProfilePurposeType, - OCPP16ChargingProfile, -} from '../../../types/ocpp/1.6/ChargingProfile'; + 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 { - OCPP16StandardParametersKey, - OCPP16SupportedFeatureProfiles, -} from '../../../types/ocpp/1.6/Configuration'; -import { OCPP16DiagnosticsStatus } from '../../../types/ocpp/1.6/DiagnosticsStatus'; + type ChargingStation, + canProceedChargingProfile, + checkChargingStation, + getConfigurationKey, + getConnectorChargingProfiles, + prepareChargingProfileKind, + removeExpiredReservations, + setConfigurationKeyValue +} from '../../../charging-station/index.js' +import { OCPPError } from '../../../exception/index.js' import { - ChangeAvailabilityRequest, - ChangeConfigurationRequest, - ClearChargingProfileRequest, - DiagnosticsStatusNotificationRequest, - GetConfigurationRequest, - GetDiagnosticsRequest, + type ChangeConfigurationRequest, + type ChangeConfigurationResponse, + ErrorType, + type GenericResponse, + GenericStatus, + type GetConfigurationRequest, + type GetConfigurationResponse, + type GetDiagnosticsRequest, + type GetDiagnosticsResponse, + type IncomingRequestHandler, + type JsonType, + OCPP16AuthorizationStatus, OCPP16AvailabilityType, - OCPP16BootNotificationRequest, - OCPP16ClearCacheRequest, - OCPP16DataTransferRequest, + 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, OCPP16DataTransferVendorId, - OCPP16HeartbeatRequest, + OCPP16DiagnosticsStatus, + type OCPP16DiagnosticsStatusNotificationRequest, + type OCPP16DiagnosticsStatusNotificationResponse, + OCPP16FirmwareStatus, + type OCPP16FirmwareStatusNotificationRequest, + type OCPP16FirmwareStatusNotificationResponse, + type OCPP16GetCompositeScheduleRequest, + type OCPP16GetCompositeScheduleResponse, + type OCPP16HeartbeatRequest, + type OCPP16HeartbeatResponse, OCPP16IncomingRequestCommand, OCPP16MessageTrigger, OCPP16RequestCommand, - OCPP16StatusNotificationRequest, - OCPP16TriggerMessageRequest, - RemoteStartTransactionRequest, - RemoteStopTransactionRequest, - ResetRequest, - SetChargingProfileRequest, - UnlockConnectorRequest, -} from '../../../types/ocpp/1.6/Requests'; -import { - ChangeAvailabilityResponse, - ChangeConfigurationResponse, - ClearChargingProfileResponse, - DiagnosticsStatusNotificationResponse, - GetConfigurationResponse, - GetDiagnosticsResponse, - OCPP16BootNotificationResponse, - OCPP16DataTransferResponse, - OCPP16DataTransferStatus, - OCPP16HeartbeatResponse, - OCPP16StatusNotificationResponse, - OCPP16TriggerMessageResponse, - SetChargingProfileResponse, - UnlockConnectorResponse, -} from '../../../types/ocpp/1.6/Responses'; -import { - OCPP16AuthorizationStatus, - OCPP16AuthorizeRequest, - OCPP16AuthorizeResponse, - OCPP16StartTransactionRequest, - OCPP16StartTransactionResponse, + type OCPP16ReserveNowRequest, + type OCPP16ReserveNowResponse, + OCPP16StandardParametersKey, + type OCPP16StartTransactionRequest, + type OCPP16StartTransactionResponse, + type OCPP16StatusNotificationRequest, + type OCPP16StatusNotificationResponse, OCPP16StopTransactionReason, -} from '../../../types/ocpp/1.6/Transaction'; -import type { OCPPConfigurationKey } from '../../../types/ocpp/Configuration'; -import { ErrorType } from '../../../types/ocpp/ErrorType'; -import type { IncomingRequestHandler } from '../../../types/ocpp/Requests'; -import type { DefaultResponse } from '../../../types/ocpp/Responses'; -import Constants from '../../../utils/Constants'; -import logger from '../../../utils/Logger'; -import Utils from '../../../utils/Utils'; -import type ChargingStation from '../../ChargingStation'; -import { ChargingStationConfigurationUtils } from '../../ChargingStationConfigurationUtils'; -import { ChargingStationUtils } from '../../ChargingStationUtils'; -import OCPPIncomingRequestService from '../OCPPIncomingRequestService'; -import { OCPP16ServiceUtils } from './OCPP16ServiceUtils'; + 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/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 default class OCPP16IncomingRequestService extends OCPPIncomingRequestService { - private incomingRequestHandlers: Map; - private jsonSchemas: Map>; +export class OCPP16IncomingRequestService extends OCPPIncomingRequestService { + protected payloadValidateFunctions: Map> - public constructor() { - if (new.target?.name === moduleName) { - throw new TypeError(`Cannot construct ${new.target?.name} instances directly`); - } - super(); + 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)], - [OCPP16IncomingRequestCommand.TRIGGER_MESSAGE, this.handleRequestTriggerMessage.bind(this)], - [OCPP16IncomingRequestCommand.DATA_TRANSFER, this.handleRequestDataTransfer.bind(this)], - ]); - this.jsonSchemas = new Map>([ + [ + 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.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, - JSON.parse( - fs.readFileSync( - path.resolve( - path.dirname(fileURLToPath(import.meta.url)), - '../../../assets/json-schemas/ocpp/1.6/Reset.json' - ), - 'utf8' + this.ajv + .compile( + OCPP16ServiceUtils.parseJsonSchemaFile( + 'assets/json-schemas/ocpp/1.6/Reset.json', + moduleName, + 'constructor' + ) ) - ) as JSONSchemaType, + .bind(this) ], [ OCPP16IncomingRequestCommand.CLEAR_CACHE, - JSON.parse( - fs.readFileSync( - path.resolve( - path.dirname(fileURLToPath(import.meta.url)), - '../../../assets/json-schemas/ocpp/1.6/ClearCache.json' - ), - 'utf8' + this.ajv + .compile( + OCPP16ServiceUtils.parseJsonSchemaFile( + 'assets/json-schemas/ocpp/1.6/ClearCache.json', + moduleName, + 'constructor' + ) ) - ) as JSONSchemaType, + .bind(this) ], [ OCPP16IncomingRequestCommand.UNLOCK_CONNECTOR, - JSON.parse( - fs.readFileSync( - path.resolve( - path.dirname(fileURLToPath(import.meta.url)), - '../../../assets/json-schemas/ocpp/1.6/UnlockConnector.json' - ), - 'utf8' + this.ajv + .compile( + OCPP16ServiceUtils.parseJsonSchemaFile( + 'assets/json-schemas/ocpp/1.6/UnlockConnector.json', + moduleName, + 'constructor' + ) ) - ) as JSONSchemaType, + .bind(this) ], [ OCPP16IncomingRequestCommand.GET_CONFIGURATION, - JSON.parse( - fs.readFileSync( - path.resolve( - path.dirname(fileURLToPath(import.meta.url)), - '../../../assets/json-schemas/ocpp/1.6/GetConfiguration.json' - ), - 'utf8' + this.ajv + .compile( + OCPP16ServiceUtils.parseJsonSchemaFile( + 'assets/json-schemas/ocpp/1.6/GetConfiguration.json', + moduleName, + 'constructor' + ) ) - ) as JSONSchemaType, + .bind(this) ], [ OCPP16IncomingRequestCommand.CHANGE_CONFIGURATION, - JSON.parse( - fs.readFileSync( - path.resolve( - path.dirname(fileURLToPath(import.meta.url)), - '../../../assets/json-schemas/ocpp/1.6/ChangeConfiguration.json' - ), - 'utf8' + this.ajv + .compile( + OCPP16ServiceUtils.parseJsonSchemaFile( + 'assets/json-schemas/ocpp/1.6/ChangeConfiguration.json', + moduleName, + 'constructor' + ) ) - ) as JSONSchemaType, + .bind(this) ], [ OCPP16IncomingRequestCommand.GET_DIAGNOSTICS, - JSON.parse( - fs.readFileSync( - path.resolve( - path.dirname(fileURLToPath(import.meta.url)), - '../../../assets/json-schemas/ocpp/1.6/GetDiagnostics.json' - ), - 'utf8' + this.ajv + .compile( + OCPP16ServiceUtils.parseJsonSchemaFile( + 'assets/json-schemas/ocpp/1.6/GetDiagnostics.json', + moduleName, + 'constructor' + ) ) - ) as JSONSchemaType, + .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, - JSON.parse( - fs.readFileSync( - path.resolve( - path.dirname(fileURLToPath(import.meta.url)), - '../../../assets/json-schemas/ocpp/1.6/SetChargingProfile.json' - ), - 'utf8' + this.ajv + .compile( + OCPP16ServiceUtils.parseJsonSchemaFile( + 'assets/json-schemas/ocpp/1.6/SetChargingProfile.json', + moduleName, + 'constructor' + ) ) - ) as JSONSchemaType, + .bind(this) ], [ OCPP16IncomingRequestCommand.CLEAR_CHARGING_PROFILE, - JSON.parse( - fs.readFileSync( - path.resolve( - path.dirname(fileURLToPath(import.meta.url)), - '../../../assets/json-schemas/ocpp/1.6/ClearChargingProfile.json' - ), - 'utf8' + this.ajv + .compile( + OCPP16ServiceUtils.parseJsonSchemaFile( + 'assets/json-schemas/ocpp/1.6/ClearChargingProfile.json', + moduleName, + 'constructor' + ) ) - ) as JSONSchemaType, + .bind(this) ], [ OCPP16IncomingRequestCommand.CHANGE_AVAILABILITY, - JSON.parse( - fs.readFileSync( - path.resolve( - path.dirname(fileURLToPath(import.meta.url)), - '../../../assets/json-schemas/ocpp/1.6/ChangeAvailability.json' - ), - 'utf8' + this.ajv + .compile( + OCPP16ServiceUtils.parseJsonSchemaFile( + 'assets/json-schemas/ocpp/1.6/ChangeAvailability.json', + moduleName, + 'constructor' + ) ) - ) as JSONSchemaType, + .bind(this) ], [ OCPP16IncomingRequestCommand.REMOTE_START_TRANSACTION, - JSON.parse( - fs.readFileSync( - path.resolve( - path.dirname(fileURLToPath(import.meta.url)), - '../../../assets/json-schemas/ocpp/1.6/RemoteStartTransaction.json' - ), - 'utf8' + this.ajv + .compile( + OCPP16ServiceUtils.parseJsonSchemaFile( + 'assets/json-schemas/ocpp/1.6/RemoteStartTransaction.json', + moduleName, + 'constructor' + ) ) - ) as JSONSchemaType, + .bind(this) ], [ OCPP16IncomingRequestCommand.REMOTE_STOP_TRANSACTION, - JSON.parse( - fs.readFileSync( - path.resolve( - path.dirname(fileURLToPath(import.meta.url)), - '../../../assets/json-schemas/ocpp/1.6/RemoteStopTransaction.json' - ), - 'utf8' + this.ajv + .compile( + OCPP16ServiceUtils.parseJsonSchemaFile( + 'assets/json-schemas/ocpp/1.6/RemoteStopTransaction.json', + moduleName, + 'constructor' + ) ) - ) as JSONSchemaType, + .bind(this) ], [ OCPP16IncomingRequestCommand.TRIGGER_MESSAGE, - JSON.parse( - fs.readFileSync( - path.resolve( - path.dirname(fileURLToPath(import.meta.url)), - '../../../assets/json-schemas/ocpp/1.6/TriggerMessage.json' - ), - 'utf8' + this.ajv + .compile( + OCPP16ServiceUtils.parseJsonSchemaFile( + 'assets/json-schemas/ocpp/1.6/TriggerMessage.json', + moduleName, + 'constructor' + ) ) - ) as JSONSchemaType, + .bind(this) ], [ OCPP16IncomingRequestCommand.DATA_TRANSFER, - JSON.parse( - fs.readFileSync( - path.resolve( - path.dirname(fileURLToPath(import.meta.url)), - '../../../assets/json-schemas/ocpp/1.6/DataTransfer.json' - ), - 'utf8' + this.ajv + .compile( + OCPP16ServiceUtils.parseJsonSchemaFile( + 'assets/json-schemas/ocpp/1.6/DataTransfer.json', + moduleName, + 'constructor' + ) + ) + .bind(this) + ], + [ + OCPP16IncomingRequestCommand.UPDATE_FIRMWARE, + this.ajv + .compile( + OCPP16ServiceUtils.parseJsonSchemaFile( + 'assets/json-schemas/ocpp/1.6/UpdateFirmware.json', + moduleName, + 'constructor' + ) ) - ) as JSONSchemaType, + .bind(this) ], - ]); - this.validatePayload.bind(this); + [ + OCPP16IncomingRequestCommand.RESERVE_NOW, + this.ajv + .compile( + OCPP16ServiceUtils.parseJsonSchemaFile( + 'assets/json-schemas/ocpp/1.6/ReserveNow.json', + moduleName, + 'constructor' + ) + ) + .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) ) { @@ -307,36 +605,39 @@ export default class OCPP16IncomingRequestService extends OCPPIncomingRequestSer 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 @@ -344,24 +645,24 @@ export default class OCPP16IncomingRequestService extends OCPPIncomingRequestSer 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( @@ -369,901 +670,1059 @@ export default class OCPP16IncomingRequestService extends OCPPIncomingRequestSer 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)) { - 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 - ): DefaultResponse { - this.asyncResource - .runInAsyncScope( - chargingStation.reset.bind(chargingStation) as ( - this: ChargingStation, - ...args: any[] - ) => Promise, - chargingStation, - (commandPayload.type + 'Reset') as OCPP16StopTransactionReason - ) - .catch(() => { - /* This is intentional */ - }); + ): GenericResponse { + 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 Constants.OCPP_RESPONSE_ACCEPTED; + ) + return OCPP16Constants.OCPP_RESPONSE_ACCEPTED } - private handleRequestClearCache(chargingStation: ChargingStation): DefaultResponse { - chargingStation.authorizedTagsCache.deleteAuthorizedTags( - ChargingStationUtils.getAuthorizationFile(chargingStation.stationInfo) - ); - return Constants.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 Constants.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 Constants.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 Constants.OCPP_RESPONSE_UNLOCKED; + return OCPP16Constants.OCPP_RESPONSE_UNLOCKED } - return Constants.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 Constants.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.isEmptyArray(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 { - for (const key of commandPayload.key) { - const keyFound = ChargingStationConfigurationUtils.getConfigurationKey( - chargingStation, - key - ); - 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 Constants.OCPP_CONFIGURATION_RESPONSE_NOT_SUPPORTED; - } else if (keyToChange && keyToChange.readonly) { - return Constants.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 Constants.OCPP_CONFIGURATION_RESPONSE_REBOOT_REQUIRED; + if (keyToChange.reboot === true) { + return OCPP16Constants.OCPP_CONFIGURATION_RESPONSE_REBOOT_REQUIRED } - return Constants.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 Constants.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 Constants.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 === - ChargingProfilePurposeType.CHARGE_POINT_MAX_PROFILE && - commandPayload.connectorId !== 0 + csChargingProfiles.chargingProfilePurpose === + OCPP16ChargingProfilePurposeType.CHARGE_POINT_MAX_PROFILE && + connectorId !== 0 ) { - return Constants.OCPP_SET_CHARGING_PROFILE_RESPONSE_REJECTED; + return OCPP16Constants.OCPP_SET_CHARGING_PROFILE_RESPONSE_REJECTED } if ( - commandPayload.csChargingProfiles.chargingProfilePurpose === - ChargingProfilePurposeType.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 Constants.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 - }, dump their stack: %j`, - chargingStation.getConnectorStatus(commandPayload.connectorId).chargingProfiles - ); - return Constants.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 handleRequestGetCompositeSchedule ( + chargingStation: ChargingStation, + commandPayload: OCPP16GetCompositeScheduleRequest + ): OCPP16GetCompositeScheduleResponse { + if ( + !OCPP16ServiceUtils.checkFeatureProfile( + chargingStation, + OCPP16SupportedFeatureProfiles.SmartCharging, + OCPP16IncomingRequestCommand.GET_COMPOSITE_SCHEDULE + ) + ) { + return OCPP16Constants.OCPP_RESPONSE_REJECTED + } + const { connectorId, duration, chargingRateUnit } = commandPayload + if (!chargingStation.hasConnector(connectorId)) { + logger.error( + `${chargingStation.logPrefix()} Trying to get composite schedule to a non existing connector id ${connectorId}` + ) + return OCPP16Constants.OCPP_RESPONSE_REJECTED + } + 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 ( + isEmptyArray(connectorStatus?.chargingProfiles) && + isEmptyArray(chargingStation.getConnectorStatus(0)?.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( + private handleRequestClearChargingProfile ( chargingStation: ChargingStation, - commandPayload: ClearChargingProfileRequest - ): ClearChargingProfileResponse { + commandPayload: OCPP16ClearChargingProfileRequest + ): OCPP16ClearChargingProfileResponse { if ( - OCPP16ServiceUtils.checkFeatureProfile( + !OCPP16ServiceUtils.checkFeatureProfile( chargingStation, OCPP16SupportedFeatureProfiles.SmartCharging, OCPP16IncomingRequestCommand.CLEAR_CHARGING_PROFILE - ) === false + ) ) { - return Constants.OCPP_CLEAR_CHARGING_PROFILE_RESPONSE_UNKNOWN; + return OCPP16Constants.OCPP_CLEAR_CHARGING_PROFILE_RESPONSE_UNKNOWN } - if (chargingStation.connectors.has(commandPayload.connectorId) === false) { + 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 ${ - commandPayload.connectorId - }` - ); - return Constants.OCPP_CLEAR_CHARGING_PROFILE_RESPONSE_UNKNOWN; + `${chargingStation.logPrefix()} Trying to clear a charging profile(s) to a non existing connector id ${connectorId}` + ) + return OCPP16Constants.OCPP_CLEAR_CHARGING_PROFILE_RESPONSE_UNKNOWN } - const connectorStatus = chargingStation.getConnectorStatus(commandPayload.connectorId); - if (commandPayload.connectorId && !Utils.isEmptyArray(connectorStatus.chargingProfiles)) { - connectorStatus.chargingProfiles = []; + // 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 - }, dump their stack: %j`, - connectorStatus.chargingProfiles - ); - return Constants.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 (!commandPayload.connectorId) { - let clearedCP = false; - for (const connectorId of chargingStation.connectors.keys()) { - if (!Utils.isEmptyArray(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 on connector id ${ - commandPayload.connectorId - }, dump their stack: %j`, - connectorStatus.chargingProfiles - ); - 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 Constants.OCPP_CLEAR_CHARGING_PROFILE_RESPONSE_ACCEPTED; + return OCPP16Constants.OCPP_CLEAR_CHARGING_PROFILE_RESPONSE_ACCEPTED } } - return Constants.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 Constants.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 - ? OCPP16ChargePointStatus.AVAILABLE - : OCPP16ChargePointStatus.UNAVAILABLE; + type === OCPP16AvailabilityType.Operative + ? OCPP16ChargePointStatus.Available + : OCPP16ChargePointStatus.Unavailable if (connectorId === 0) { - let response: ChangeAvailabilityResponse = Constants.OCPP_AVAILABILITY_RESPONSE_ACCEPTED; - for (const id of chargingStation.connectors.keys()) { - if (chargingStation.getConnectorStatus(id)?.transactionStarted === true) { - response = Constants.OCPP_AVAILABILITY_RESPONSE_SCHEDULED; - } - chargingStation.getConnectorStatus(id).availability = commandPayload.type; - if (response === Constants.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 Constants.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 Constants.OCPP_AVAILABILITY_RESPONSE_ACCEPTED; + chargePointStatus + ) + return OCPP16Constants.OCPP_AVAILABILITY_RESPONSE_ACCEPTED } - return Constants.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 && - 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 Constants.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 Constants.OCPP_RESPONSE_ACCEPTED; - } - return this.notifyRemoteStartTransactionRejected( - chargingStation, - transactionConnectorId, - commandPayload.idTag - ); - } - return this.notifyRemoteStartTransactionRejected( - chargingStation, - transactionConnectorId, - commandPayload.idTag - ); - } - return this.notifyRemoteStartTransactionRejected( + ): Promise { + 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, { + ): Promise { + 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 Constants.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 === ChargingProfilePurposeType.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}, dump their stack: %j`, - chargingStation.getConnectorStatus(connectorId).chargingProfiles - ); - return true; - } else if (cp && cp.chargingProfilePurpose !== ChargingProfilePurposeType.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; + `${chargingStation.logPrefix()} Charging profile(s) set at remote start transaction on connector id ${connectorId}: %j`, + 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 Constants.OCPP_RESPONSE_ACCEPTED; + ): 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.debug( + `${chargingStation.logPrefix()} Remote stop transaction REJECTED for transactionId '${transactionId}'` + ) + return OCPP16Constants.OCPP_RESPONSE_REJECTED + } + + private handleRequestUpdateFirmware ( + chargingStation: ChargingStation, + commandPayload: OCPP16UpdateFirmwareRequest + ): OCPP16UpdateFirmwareResponse { + if ( + !OCPP16ServiceUtils.checkFeatureProfile( + chargingStation, + OCPP16SupportedFeatureProfiles.FirmwareManagement, + OCPP16IncomingRequestCommand.UPDATE_FIRMWARE + ) + ) { + logger.warn( + `${chargingStation.logPrefix()} ${moduleName}.handleRequestUpdateFirmware: Cannot simulate firmware update: feature profile not supported` + ) + return OCPP16Constants.OCPP_RESPONSE_EMPTY + } + // 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 OCPP16Constants.OCPP_RESPONSE_EMPTY + } + const now = Date.now() + if (retrieveDate.getTime() <= now) { + this.updateFirmwareSimulation(chargingStation).catch(Constants.EMPTY_FUNCTION) + } else { + setTimeout(() => { + this.updateFirmwareSimulation(chargingStation).catch(Constants.EMPTY_FUNCTION) + }, retrieveDate.getTime() - now) + } + return OCPP16Constants.OCPP_RESPONSE_EMPTY + } + + private async updateFirmwareSimulation ( + chargingStation: ChargingStation, + maxDelay = 30, + minDelay = 15 + ): Promise { + 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 + ) } - return Constants.OCPP_RESPONSE_REJECTED; } } - logger.warn( - chargingStation.logPrefix() + - ' Trying to remote stop a non existing transaction ' + - transactionId.toString() - ); - return Constants.OCPP_RESPONSE_REJECTED; + 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 === + OCPP16FirmwareStatus.DownloadFailed + ) { + await sleep(secondsToMilliseconds(getRandomInteger(maxDelay, minDelay))) + await chargingStation.ocppRequestService.requestHandler< + OCPP16FirmwareStatusNotificationRequest, + OCPP16FirmwareStatusNotificationResponse + >(chargingStation, OCPP16RequestCommand.FIRMWARE_STATUS_NOTIFICATION, { + 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 + >(chargingStation, OCPP16RequestCommand.FIRMWARE_STATUS_NOTIFICATION, { + 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 + >(chargingStation, OCPP16RequestCommand.FIRMWARE_STATUS_NOTIFICATION, { + 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: chargingStation.stationInfo.firmwareUpgrade.failureStatus + }) + chargingStation.stationInfo.firmwareStatus = + chargingStation.stationInfo.firmwareUpgrade.failureStatus + return + } + if (chargingStation.stationInfo?.firmwareUpgrade?.reset === true) { + 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 + ) ) { - return Constants.OCPP_RESPONSE_EMPTY; + logger.warn( + `${chargingStation.logPrefix()} ${moduleName}.handleRequestGetDiagnostics: Cannot get diagnostics: feature profile not supported` + ) + return OCPP16Constants.OCPP_RESPONSE_EMPTY } - logger.debug( - chargingStation.logPrefix() + - ' ' + - OCPP16IncomingRequestCommand.GET_DIAGNOSTICS + - ' request received: %j', - commandPayload - ); - 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.isEmptyString(uri.port) && { port: Utils.convertToInt(uri.port) }), - ...(!Utils.isEmptyString(uri.username) && { user: uri.username }), - ...(!Utils.isEmptyString(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) { - // eslint-disable-next-line @typescript-eslint/no-misused-promises - ftpClient.trackProgress(async (info) => { + ftpClient.trackProgress(info => { logger.info( - `${chargingStation.logPrefix()} ${ + `${chargingStation.logPrefix()} ${moduleName}.handleRequestGetDiagnostics: ${ info.bytes / 1024 } bytes transferred from diagnostics archive ${info.name}` - ); - await chargingStation.ocppRequestService.requestHandler< - DiagnosticsStatusNotificationRequest, - DiagnosticsStatusNotificationResponse + ) + chargingStation.ocppRequestService + .requestHandler< + OCPP16DiagnosticsStatusNotificationRequest, + OCPP16DiagnosticsStatusNotificationResponse >(chargingStation, OCPP16RequestCommand.DIAGNOSTICS_STATUS_NOTIFICATION, { - status: OCPP16DiagnosticsStatus.Uploading, - }); - }); + 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 - ), - uri.pathname + diagnosticsArchive - ); + join(resolve(dirname(fileURLToPath(import.meta.url)), '../'), diagnosticsArchive), + `${uri.pathname}${diagnosticsArchive}` + ) if (uploadResponse.code === 226) { await chargingStation.ocppRequestService.requestHandler< - DiagnosticsStatusNotificationRequest, - DiagnosticsStatusNotificationResponse + 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< - DiagnosticsStatusNotificationRequest, - DiagnosticsStatusNotificationResponse + 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: Constants.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< - DiagnosticsStatusNotificationRequest, - DiagnosticsStatusNotificationResponse + OCPP16DiagnosticsStatusNotificationRequest, + OCPP16DiagnosticsStatusNotificationResponse >(chargingStation, OCPP16RequestCommand.DIAGNOSTICS_STATUS_NOTIFICATION, { - status: OCPP16DiagnosticsStatus.UploadFailed, - }); - return Constants.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 Constants.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 Constants.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(() => { - /* This is intentional */ - }); - }, Constants.OCPP_TRIGGER_MESSAGE_DELAY); - return Constants.OCPP_TRIGGER_MESSAGE_RESPONSE_ACCEPTED; - case OCPP16MessageTrigger.Heartbeat: - setTimeout(() => { - chargingStation.ocppRequestService - .requestHandler( - chargingStation, - OCPP16RequestCommand.HEARTBEAT, - null, - { - triggerMessage: true, - } - ) - .catch(() => { - /* This is intentional */ - }); - }, Constants.OCPP_TRIGGER_MESSAGE_DELAY); - return Constants.OCPP_TRIGGER_MESSAGE_RESPONSE_ACCEPTED; - case OCPP16MessageTrigger.StatusNotification: - setTimeout(() => { - if (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(() => { - /* This is intentional */ - }); - } 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(() => { - /* This is intentional */ - }); - } - } - }, Constants.OCPP_TRIGGER_MESSAGE_DELAY); - return Constants.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 Constants.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: Constants.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: Constants.OCPP_DATA_TRANSFER_RESPONSE_REJECTED } - ); + { errorResponse: OCPP16Constants.OCPP_CANCEL_RESERVATION_RESPONSE_REJECTED } + )! } } }