fix: ensure inflight requests id cannot be duplicated
[e-mobility-charging-stations-simulator.git] / src / charging-station / ocpp / 1.6 / OCPP16IncomingRequestService.ts
index a61cb21995b2bd845e2eb0519f10007cd5febf08..fe25221050a8afa02e2f4b0091217c5726116b2b 100644 (file)
@@ -1,5 +1,6 @@
 // Partial Copyright Jerome Benoit. 2021-2024. All Rights Reserved.
 
+import { randomInt } from 'node:crypto'
 import { createWriteStream, readdirSync } from 'node:fs'
 import { dirname, extname, join, resolve } from 'node:path'
 import { fileURLToPath, URL } from 'node:url'
@@ -14,6 +15,7 @@ import {
   secondsToMilliseconds
 } from 'date-fns'
 import { maxTime } from 'date-fns/constants'
+import { isEmpty } from 'rambda'
 import { create } from 'tar'
 
 import {
@@ -24,6 +26,7 @@ import {
   getConnectorChargingProfiles,
   prepareChargingProfileKind,
   removeExpiredReservations,
+  resetAuthorizeConnectorStatus,
   setConfigurationKeyValue
 } from '../../../charging-station/index.js'
 import { OCPPError } from '../../../exception/index.js'
@@ -103,9 +106,7 @@ import {
   convertToDate,
   convertToInt,
   formatDurationMilliSeconds,
-  getRandomInteger,
   isAsyncFunction,
-  isEmptyArray,
   isNotEmptyArray,
   isNotEmptyString,
   logger,
@@ -420,9 +421,9 @@ export class OCPP16IncomingRequestService extends OCPPIncomingRequestService {
         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.getConnectorStatus(connectorId!)!.transactionRemoteStarted = true
           chargingStation.ocppRequestService
-            .requestHandler<OCPP16StartTransactionRequest, OCPP16StartTransactionResponse>(
+            .requestHandler<Partial<OCPP16StartTransactionRequest>, OCPP16StartTransactionResponse>(
             chargingStation,
             OCPP16RequestCommand.START_TRANSACTION,
             {
@@ -431,17 +432,21 @@ export class OCPP16IncomingRequestService extends OCPPIncomingRequestService {
             }
           )
             .then(response => {
-              if (response.status === OCPP16AuthorizationStatus.ACCEPTED) {
+              if (response.idTagInfo.status === OCPP16AuthorizationStatus.ACCEPTED) {
                 logger.debug(
-                  `${chargingStation.logPrefix()} Remote start transaction ACCEPTED on ${chargingStation.stationInfo?.chargingStationId}#${connectorId} for idTag '${idTag}'`
+                  `${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}'`
+                  `${chargingStation.logPrefix()} Remote start transaction REJECTED on ${
+                    chargingStation.stationInfo?.chargingStationId
+                  }#${connectorId} for idTag '${idTag}'`
                 )
               }
             })
-            .catch(error => {
+            .catch((error: unknown) => {
               logger.error(
                 `${chargingStation.logPrefix()} ${moduleName}.constructor: Remote start transaction error:`,
                 error
@@ -465,15 +470,19 @@ export class OCPP16IncomingRequestService extends OCPPIncomingRequestService {
             .then(response => {
               if (response.status === GenericStatus.Accepted) {
                 logger.debug(
-                  `${chargingStation.logPrefix()} Remote stop transaction ACCEPTED on ${chargingStation.stationInfo?.chargingStationId}#${connectorId} for transaction '${transactionId}'`
+                  `${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}'`
+                  `${chargingStation.logPrefix()} Remote stop transaction REJECTED on ${
+                    chargingStation.stationInfo?.chargingStationId
+                  }#${connectorId} for transaction '${transactionId}'`
                 )
               }
             })
-            .catch(error => {
+            .catch((error: unknown) => {
               logger.error(
                 `${chargingStation.logPrefix()} ${moduleName}.constructor: Remote stop transaction error:`,
                 error
@@ -493,7 +502,7 @@ export class OCPP16IncomingRequestService extends OCPPIncomingRequestService {
           return
         }
         const { requestedMessage, connectorId } = request
-        const errorHandler = (error: Error): void => {
+        const errorHandler = (error: unknown): void => {
           logger.error(
             `${chargingStation.logPrefix()} ${moduleName}.constructor: Trigger ${requestedMessage} error:`,
             error
@@ -502,15 +511,10 @@ export class OCPP16IncomingRequestService extends OCPPIncomingRequestService {
         switch (requestedMessage) {
           case OCPP16MessageTrigger.BootNotification:
             chargingStation.ocppRequestService
-              .requestHandler<OCPP16BootNotificationRequest, OCPP16BootNotificationResponse>(
-              chargingStation,
-              OCPP16RequestCommand.BOOT_NOTIFICATION,
-              chargingStation.bootNotificationRequest,
-              { skipBufferingOnError: true, triggerMessage: true }
-            )
-              .then(response => {
-                chargingStation.bootNotificationResponse = response
-              })
+              .requestHandler<
+            OCPP16BootNotificationRequest,
+            OCPP16BootNotificationResponse
+            >(chargingStation, OCPP16RequestCommand.BOOT_NOTIFICATION, chargingStation.bootNotificationRequest as OCPP16BootNotificationRequest, { skipBufferingOnError: true, triggerMessage: true })
               .catch(errorHandler)
             break
           case OCPP16MessageTrigger.Heartbeat:
@@ -534,7 +538,8 @@ export class OCPP16IncomingRequestService extends OCPPIncomingRequestService {
                 {
                   connectorId,
                   errorCode: OCPP16ChargePointErrorCode.NO_ERROR,
-                  status: chargingStation.getConnectorStatus(connectorId)?.status
+                  status: chargingStation.getConnectorStatus(connectorId)
+                    ?.status as OCPP16ChargePointStatus
                 },
                 {
                   triggerMessage: true
@@ -554,7 +559,7 @@ export class OCPP16IncomingRequestService extends OCPPIncomingRequestService {
                     {
                       connectorId: id,
                       errorCode: OCPP16ChargePointErrorCode.NO_ERROR,
-                      status: connectorStatus.status
+                      status: connectorStatus.status as OCPP16ChargePointStatus
                     },
                     {
                       triggerMessage: true
@@ -575,7 +580,7 @@ export class OCPP16IncomingRequestService extends OCPPIncomingRequestService {
                   {
                     connectorId: id,
                     errorCode: OCPP16ChargePointErrorCode.NO_ERROR,
-                    status: connectorStatus.status
+                    status: connectorStatus.status as OCPP16ChargePointStatus
                   },
                   {
                     triggerMessage: true
@@ -646,7 +651,7 @@ export class OCPP16IncomingRequestService extends OCPPIncomingRequestService {
         // Throw exception
         throw new OCPPError(
           ErrorType.NOT_IMPLEMENTED,
-          `'${commandName}' is not implemented to handle request PDU ${JSON.stringify(
+          `${commandName} is not implemented to handle request PDU ${JSON.stringify(
             commandPayload,
             undefined,
             2
@@ -943,8 +948,8 @@ export class OCPP16IncomingRequestService extends OCPPIncomingRequestService {
     }
     const connectorStatus = chargingStation.getConnectorStatus(connectorId)
     if (
-      isEmptyArray(connectorStatus?.chargingProfiles) &&
-      isEmptyArray(chargingStation.getConnectorStatus(0)?.chargingProfiles)
+      isEmpty(connectorStatus?.chargingProfiles) &&
+      isEmpty(chargingStation.getConnectorStatus(0)?.chargingProfiles)
     ) {
       return OCPP16Constants.OCPP_RESPONSE_REJECTED
     }
@@ -1044,41 +1049,46 @@ export class OCPP16IncomingRequestService extends OCPPIncomingRequestService {
       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 ${connectorId}`
-      )
-      return OCPP16Constants.OCPP_CLEAR_CHARGING_PROFILE_RESPONSE_ACCEPTED
-    }
-    if (connectorId == null) {
+    if (connectorId != null) {
+      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
+      }
+      const connectorStatus = chargingStation.getConnectorStatus(connectorId)
+      if (isNotEmptyArray(connectorStatus?.chargingProfiles)) {
+        connectorStatus.chargingProfiles = []
+        logger.debug(
+          `${chargingStation.logPrefix()} Charging profile(s) cleared on connector id ${connectorId}`
+        )
+        return OCPP16Constants.OCPP_CLEAR_CHARGING_PROFILE_RESPONSE_ACCEPTED
+      }
+    } else {
       let clearedCP = false
       if (chargingStation.hasEvses) {
         for (const evseStatus of chargingStation.evses.values()) {
           for (const status of evseStatus.connectors.values()) {
-            clearedCP = OCPP16ServiceUtils.clearChargingProfiles(
+            const clearedConnectorCP = OCPP16ServiceUtils.clearChargingProfiles(
               chargingStation,
               commandPayload,
               status.chargingProfiles
             )
+            if (clearedConnectorCP && !clearedCP) {
+              clearedCP = true
+            }
           }
         }
       } else {
         for (const id of chargingStation.connectors.keys()) {
-          clearedCP = OCPP16ServiceUtils.clearChargingProfiles(
+          const clearedConnectorCP = OCPP16ServiceUtils.clearChargingProfiles(
             chargingStation,
             commandPayload,
             chargingStation.getConnectorStatus(id)?.chargingProfiles
           )
+          if (clearedConnectorCP && !clearedCP) {
+            clearedCP = true
+          }
         }
       }
       if (clearedCP) {
@@ -1151,9 +1161,22 @@ export class OCPP16IncomingRequestService extends OCPPIncomingRequestService {
     chargingStation: ChargingStation,
     commandPayload: RemoteStartTransactionRequest
   ): Promise<GenericResponse> {
+    if (commandPayload.connectorId == null) {
+      do {
+        commandPayload.connectorId = randomInt(1, chargingStation.getNumberOfConnectors())
+      } while (
+        chargingStation.getConnectorStatus(commandPayload.connectorId)?.transactionStarted ===
+          true &&
+        OCPP16ServiceUtils.hasReservation(
+          chargingStation,
+          commandPayload.connectorId,
+          commandPayload.idTag
+        )
+      )
+    }
     const { connectorId: transactionConnectorId, idTag, chargingProfile } = commandPayload
     if (!chargingStation.hasConnector(transactionConnectorId)) {
-      return await this.notifyRemoteStartTransactionRejected(
+      return this.notifyRemoteStartTransactionRejected(
         chargingStation,
         transactionConnectorId,
         idTag
@@ -1163,7 +1186,7 @@ export class OCPP16IncomingRequestService extends OCPPIncomingRequestService {
       !chargingStation.isChargingStationAvailable() ||
       !chargingStation.isConnectorAvailable(transactionConnectorId)
     ) {
-      return await this.notifyRemoteStartTransactionRejected(
+      return this.notifyRemoteStartTransactionRejected(
         chargingStation,
         transactionConnectorId,
         idTag
@@ -1174,17 +1197,12 @@ export class OCPP16IncomingRequestService extends OCPPIncomingRequestService {
       chargingStation.getAuthorizeRemoteTxRequests() &&
       !(await OCPP16ServiceUtils.isIdTagAuthorized(chargingStation, transactionConnectorId, idTag))
     ) {
-      return await this.notifyRemoteStartTransactionRejected(
+      return this.notifyRemoteStartTransactionRejected(
         chargingStation,
         transactionConnectorId,
         idTag
       )
     }
-    await OCPP16ServiceUtils.sendAndSetConnectorStatus(
-      chargingStation,
-      transactionConnectorId,
-      OCPP16ChargePointStatus.Preparing
-    )
     if (
       chargingProfile != null &&
       !this.setRemoteStartTransactionChargingProfile(
@@ -1193,33 +1211,32 @@ export class OCPP16IncomingRequestService extends OCPPIncomingRequestService {
         chargingProfile
       )
     ) {
-      return await this.notifyRemoteStartTransactionRejected(
+      return this.notifyRemoteStartTransactionRejected(
         chargingStation,
         transactionConnectorId,
         idTag
       )
     }
     logger.debug(
-      `${chargingStation.logPrefix()} Remote start transaction ACCEPTED on connector id ${transactionConnectorId}, idTag '${idTag}'`
+      `${chargingStation.logPrefix()} Remote start transaction ACCEPTED on ${
+        chargingStation.stationInfo?.chargingStationId
+      }#${transactionConnectorId}}, idTag '${idTag}'`
     )
     return OCPP16Constants.OCPP_RESPONSE_ACCEPTED
   }
 
-  private async notifyRemoteStartTransactionRejected (
+  private notifyRemoteStartTransactionRejected (
     chargingStation: ChargingStation,
     connectorId: number,
     idTag: string
-  ): Promise<GenericResponse> {
+  ): GenericResponse {
     const connectorStatus = chargingStation.getConnectorStatus(connectorId)
-    if (connectorStatus?.status !== OCPP16ChargePointStatus.Available) {
-      await OCPP16ServiceUtils.sendAndSetConnectorStatus(
-        chargingStation,
-        connectorId,
-        OCPP16ChargePointStatus.Available
-      )
-    }
     logger.debug(
-      `${chargingStation.logPrefix()} Remote start transaction REJECTED on connector id ${connectorId}, idTag '${idTag}', availability '${connectorStatus?.availability}', status '${connectorStatus?.status}'`
+      `${chargingStation.logPrefix()} Remote start transaction REJECTED on ${
+        chargingStation.stationInfo?.chargingStationId
+      }#${connectorId}, idTag '${idTag}', availability '${
+        connectorStatus?.availability
+      }', status '${connectorStatus?.status}'`
     )
     return OCPP16Constants.OCPP_RESPONSE_REJECTED
   }
@@ -1232,7 +1249,9 @@ export class OCPP16IncomingRequestService extends OCPPIncomingRequestService {
     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`,
+        `${chargingStation.logPrefix()} Charging profile(s) set at remote start transaction on ${
+          chargingStation.stationInfo?.chargingStationId
+        }#${connectorId}`,
         chargingProfile
       )
       return true
@@ -1281,7 +1300,10 @@ export class OCPP16IncomingRequestService extends OCPPIncomingRequestService {
     // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
     commandPayload.retrieveDate = convertToDate(commandPayload.retrieveDate)!
     const { retrieveDate } = commandPayload
-    if (chargingStation.stationInfo?.firmwareStatus !== OCPP16FirmwareStatus.Installed) {
+    if (
+      chargingStation.stationInfo?.firmwareStatus != null &&
+      chargingStation.stationInfo.firmwareStatus !== OCPP16FirmwareStatus.Installed
+    ) {
       logger.warn(
         `${chargingStation.logPrefix()} ${moduleName}.handleRequestUpdateFirmware: Cannot simulate firmware update: firmware update is already in progress`
       )
@@ -1346,7 +1368,7 @@ export class OCPP16IncomingRequestService extends OCPPIncomingRequestService {
       chargingStation.stationInfo?.firmwareUpgrade?.failureStatus ===
       OCPP16FirmwareStatus.DownloadFailed
     ) {
-      await sleep(secondsToMilliseconds(getRandomInteger(maxDelay, minDelay)))
+      await sleep(secondsToMilliseconds(randomInt(minDelay, maxDelay)))
       await chargingStation.ocppRequestService.requestHandler<
       OCPP16FirmwareStatusNotificationRequest,
       OCPP16FirmwareStatusNotificationResponse
@@ -1357,7 +1379,7 @@ export class OCPP16IncomingRequestService extends OCPPIncomingRequestService {
         chargingStation.stationInfo.firmwareUpgrade.failureStatus
       return
     }
-    await sleep(secondsToMilliseconds(getRandomInteger(maxDelay, minDelay)))
+    await sleep(secondsToMilliseconds(randomInt(minDelay, maxDelay)))
     await chargingStation.ocppRequestService.requestHandler<
     OCPP16FirmwareStatusNotificationRequest,
     OCPP16FirmwareStatusNotificationResponse
@@ -1413,8 +1435,7 @@ export class OCPP16IncomingRequestService extends OCPPIncomingRequestService {
         transactionsStarted = false
       }
     } while (transactionsStarted)
-    !wasTransactionsStarted &&
-      (await sleep(secondsToMilliseconds(getRandomInteger(maxDelay, minDelay))))
+    !wasTransactionsStarted && (await sleep(secondsToMilliseconds(randomInt(minDelay, maxDelay))))
     if (!checkChargingStation(chargingStation, chargingStation.logPrefix())) {
       return
     }
@@ -1430,7 +1451,7 @@ export class OCPP16IncomingRequestService extends OCPPIncomingRequestService {
       chargingStation.stationInfo?.firmwareUpgrade?.failureStatus ===
       OCPP16FirmwareStatus.InstallationFailed
     ) {
-      await sleep(secondsToMilliseconds(getRandomInteger(maxDelay, minDelay)))
+      await sleep(secondsToMilliseconds(randomInt(minDelay, maxDelay)))
       await chargingStation.ocppRequestService.requestHandler<
       OCPP16FirmwareStatusNotificationRequest,
       OCPP16FirmwareStatusNotificationResponse
@@ -1442,7 +1463,7 @@ export class OCPP16IncomingRequestService extends OCPPIncomingRequestService {
       return
     }
     if (chargingStation.stationInfo?.firmwareUpgrade?.reset === true) {
-      await sleep(secondsToMilliseconds(getRandomInteger(maxDelay, minDelay)))
+      await sleep(secondsToMilliseconds(randomInt(minDelay, maxDelay)))
       await chargingStation.reset(OCPP16StopTransactionReason.REBOOT)
     }
   }
@@ -1471,7 +1492,10 @@ export class OCPP16IncomingRequestService extends OCPPIncomingRequestService {
         const logConfiguration = Configuration.getConfigurationSection<LogConfiguration>(
           ConfigurationSection.log
         )
-        const logFiles = readdirSync(resolve(dirname(fileURLToPath(import.meta.url)), '../'))
+        const logFiles = readdirSync(
+          // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
+          resolve((fileURLToPath(import.meta.url), '../', dirname(logConfiguration.file!)))
+        )
           // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
           .filter(file => file.endsWith(extname(logConfiguration.file!)))
           // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
@@ -1500,7 +1524,7 @@ export class OCPP16IncomingRequestService extends OCPPIncomingRequestService {
             >(chargingStation, OCPP16RequestCommand.DIAGNOSTICS_STATUS_NOTIFICATION, {
               status: OCPP16DiagnosticsStatus.Uploading
             })
-              .catch(error => {
+              .catch((error: unknown) => {
                 logger.error(
                   `${chargingStation.logPrefix()} ${moduleName}.handleRequestGetDiagnostics: Error while sending '${
                     OCPP16RequestCommand.DIAGNOSTICS_STATUS_NOTIFICATION
@@ -1638,19 +1662,28 @@ export class OCPP16IncomingRequestService extends OCPPIncomingRequestService {
     // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
     commandPayload.expiryDate = convertToDate(commandPayload.expiryDate)!
     const { reservationId, idTag, connectorId } = commandPayload
+    if (!chargingStation.hasConnector(connectorId)) {
+      logger.error(
+        `${chargingStation.logPrefix()} Trying to reserve a non existing connector id ${connectorId}`
+      )
+      return OCPP16Constants.OCPP_RESERVATION_RESPONSE_REJECTED
+    }
+    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
+    }
+    // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
+    const connectorStatus = chargingStation.getConnectorStatus(connectorId)!
+    resetAuthorizeConnectorStatus(connectorStatus)
     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) {
+      switch (connectorStatus.status) {
         case OCPP16ChargePointStatus.Faulted:
           response = OCPP16Constants.OCPP_RESERVATION_RESPONSE_FAULTED
           break
@@ -1729,7 +1762,9 @@ export class OCPP16IncomingRequestService extends OCPPIncomingRequestService {
         chargingStation,
         OCPP16IncomingRequestCommand.CANCEL_RESERVATION,
         error as Error,
-        { errorResponse: OCPP16Constants.OCPP_CANCEL_RESERVATION_RESPONSE_REJECTED }
+        {
+          errorResponse: OCPP16Constants.OCPP_CANCEL_RESERVATION_RESPONSE_REJECTED
+        }
       )!
     }
   }