]> Piment Noir Git Repositories - e-mobility-charging-stations-simulator.git/commitdiff
refactor(ocpp): remove as-unknown-as double-casts from request/response dispatch...
authorJérôme Benoit <jerome.benoit@piment-noir.org>
Sun, 12 Jul 2026 19:54:24 +0000 (21:54 +0200)
committerGitHub <noreply@github.com>
Sun, 12 Jul 2026 19:54:24 +0000 (21:54 +0200)
Give each builder its precise input contract instead of laundering casts
through phantom generics or JsonType:

- buildRequestPayload drops its unsound <Request> generic and returns honest
  JsonType; each dispatch branch narrows with a single cast at a genuine
  JsonType boundary.
- Builder inputs (StatusNotificationOptions, OCPP20TransactionEventOptions,
  SignCertificateOptions) are JsonObject subtypes; StatusNotification build
  validates connectorStatus via the shared isOCPP20ConnectorStatus guard.
- Handler bridges (toRequestHandler/toResponseHandler) use a single cast that
  preserves the handler's async identity, fixing an unawaited-handler
  regression from a plain wrapper defeating isAsyncFunction.
- CAT-C string-to-enum narrowing becomes a type guard / typed Record.

Closes #1968

19 files changed:
src/charging-station/ChargingStation.ts
src/charging-station/broadcast-channel/ChargingStationWorkerBroadcastChannel.ts
src/charging-station/ocpp/1.6/OCPP16IncomingRequestService.ts
src/charging-station/ocpp/1.6/OCPP16RequestService.ts
src/charging-station/ocpp/1.6/OCPP16ResponseService.ts
src/charging-station/ocpp/1.6/OCPP16ServiceUtils.ts
src/charging-station/ocpp/2.0/OCPP20IncomingRequestService.ts
src/charging-station/ocpp/2.0/OCPP20RequestService.ts
src/charging-station/ocpp/2.0/OCPP20ResponseService.ts
src/charging-station/ocpp/2.0/OCPP20ServiceUtils.ts
src/charging-station/ocpp/OCPPConnectorStatusOperations.ts
src/charging-station/ocpp/OCPPIncomingRequestService.ts
src/charging-station/ocpp/OCPPResponseService.ts
src/charging-station/ocpp/auth/types/AuthTypes.ts
src/types/index.ts
src/types/ocpp/2.0/Transaction.ts
src/types/ocpp/Requests.ts
tests/charging-station/ocpp/2.0/OCPP20ServiceUtils-StatusNotification.test.ts
tests/charging-station/ocpp/OCPPConnectorStatusOperations.test.ts

index 95234b40f667d6e0b4a92b43d1e614f5c31a4314..cae04dd6a3a641b41db2da46bfb5891651fe154a 100644 (file)
@@ -58,7 +58,6 @@ import {
   type Response,
   StandardParametersKey,
   type Status,
-  type StatusNotificationRequest,
   type StopTransactionReason,
   SupervisionUrlDistribution,
   SupportedFeatureProfiles,
@@ -345,7 +344,7 @@ export class ChargingStation extends EventEmitter {
       {
         connectorId: reservation.connectorId,
         status: ConnectorStatusEnum.Reserved,
-      } as unknown as StatusNotificationRequest,
+      },
       { send: reservation.connectorId !== 0 }
     )
   }
@@ -1038,7 +1037,7 @@ export class ChargingStation extends EventEmitter {
           {
             connectorId: reservation.connectorId,
             status: ConnectorStatusEnum.Available,
-          } as unknown as StatusNotificationRequest,
+          },
           { send: reservation.connectorId !== 0 }
         )
         delete connectorStatus.reservation
@@ -2846,7 +2845,7 @@ export class ChargingStation extends EventEmitter {
         connectorId,
         ...(evseId != null && { evseId }),
         status: getBootConnectorStatus(this, connectorId, connectorStatus),
-      } as unknown as StatusNotificationRequest)
+      })
     }
     if (this.stationInfo?.firmwareStatus === FirmwareStatus.Installing) {
       await this.ocppRequestService.requestHandler<
@@ -2911,7 +2910,7 @@ export class ChargingStation extends EventEmitter {
         connectorId,
         ...(evseId != null && { evseId }),
         status: ConnectorStatusEnum.Unavailable,
-      } as unknown as StatusNotificationRequest)
+      })
       delete connectorStatus.status
     }
   }
index b3f1f7511186a26912f9c094152da897da890424..b4aa7aa12434f8705dbee2342103842e245cdacb 100644 (file)
@@ -36,7 +36,7 @@ import {
   ResponseStatus,
   StandardParametersKey,
   type StartTransactionResponse,
-  type StatusNotificationRequest,
+  type StatusNotificationOptions,
   type StopTransactionRequest,
   type StopTransactionResponse,
 } from '../../types/index.js'
@@ -471,16 +471,13 @@ export class ChargingStationWorkerBroadcastChannel extends WorkerBroadcastChanne
         `${this.chargingStation.logPrefix()} ${moduleName}.handleStatusNotification: 'connectorId' field is required`
       )
     }
-    const payload = requestPayload as Record<string, unknown>
-    if (payload.connectorStatus == null && payload.status == null) {
+    const options = requestPayload as StatusNotificationOptions
+    if (options.connectorStatus == null && options.status == null) {
       throw new BaseError(
         `${this.chargingStation.logPrefix()} ${moduleName}.handleStatusNotification: 'connectorStatus' or 'status' field is required`
       )
     }
-    await sendAndSetConnectorStatus(
-      this.chargingStation,
-      requestPayload as unknown as StatusNotificationRequest
-    )
+    await sendAndSetConnectorStatus(this.chargingStation, options)
   }
 
   private async handleStopTransaction (
index 5b124e53ef495fa22dc1b723e8a085540138f52e..208cabf05af1cf31311e2d0a2ed7e6650b80f01c 100644 (file)
@@ -85,7 +85,6 @@ import {
   OCPP16StandardParametersKey,
   type OCPP16StartTransactionRequest,
   type OCPP16StartTransactionResponse,
-  type OCPP16StatusNotificationRequest,
   type OCPP16StatusNotificationResponse,
   OCPP16StopTransactionReason,
   OCPP16SupportedFeatureProfiles,
@@ -103,6 +102,7 @@ import {
   type ResetRequest,
   type SetChargingProfileRequest,
   type SetChargingProfileResponse,
+  type StatusNotificationOptions,
   type UnlockConnectorRequest,
   type UnlockConnectorResponse,
 } from '../../../types/index.js'
@@ -639,14 +639,14 @@ export class OCPP16IncomingRequestService extends OCPPIncomingRequestService<OCP
           case OCPP16MessageTrigger.StatusNotification:
             if (connectorId != null) {
               chargingStation.ocppRequestService
-                .requestHandler<OCPP16StatusNotificationRequest, OCPP16StatusNotificationResponse>(
+                .requestHandler<StatusNotificationOptions, OCPP16StatusNotificationResponse>(
                   chargingStation,
                   OCPP16RequestCommand.STATUS_NOTIFICATION,
                   {
                     connectorId,
                     status: chargingStation.getConnectorStatus(connectorId)
                       ?.status as OCPP16ChargePointStatus,
-                  } as unknown as OCPP16StatusNotificationRequest,
+                  },
                   {
                     triggerMessage: true,
                   }
@@ -655,16 +655,13 @@ export class OCPP16IncomingRequestService extends OCPPIncomingRequestService<OCP
             } else {
               for (const { connectorId, connectorStatus } of chargingStation.iterateConnectors()) {
                 chargingStation.ocppRequestService
-                  .requestHandler<
-                    OCPP16StatusNotificationRequest,
-                    OCPP16StatusNotificationResponse
-                  >(
+                  .requestHandler<StatusNotificationOptions, OCPP16StatusNotificationResponse>(
                     chargingStation,
                     OCPP16RequestCommand.STATUS_NOTIFICATION,
                     {
                       connectorId,
                       status: connectorStatus.status as OCPP16ChargePointStatus,
-                    } as unknown as OCPP16StatusNotificationRequest,
+                    },
                     {
                       triggerMessage: true,
                     }
@@ -945,7 +942,7 @@ export class OCPP16IncomingRequestService extends OCPPIncomingRequestService<OCP
       await sendAndSetConnectorStatus(chargingStation, {
         connectorId,
         status: chargePointStatus,
-      } as OCPP16StatusNotificationRequest)
+      })
       return OCPP16Constants.OCPP_AVAILABILITY_RESPONSE_ACCEPTED
     }
     return OCPP16Constants.OCPP_AVAILABILITY_RESPONSE_REJECTED
@@ -1942,7 +1939,7 @@ export class OCPP16IncomingRequestService extends OCPPIncomingRequestService<OCP
     await sendAndSetConnectorStatus(chargingStation, {
       connectorId,
       status: OCPP16ChargePointStatus.Available,
-    } as OCPP16StatusNotificationRequest)
+    })
     chargingStation.unlockConnector(connectorId)
     return OCPP16Constants.OCPP_RESPONSE_UNLOCKED
   }
@@ -2047,7 +2044,7 @@ export class OCPP16IncomingRequestService extends OCPPIncomingRequestService<OCP
           await sendAndSetConnectorStatus(chargingStation, {
             connectorId,
             status: OCPP16ChargePointStatus.Unavailable,
-          } as OCPP16StatusNotificationRequest)
+          })
         }
       }
       await chargingStation.ocppRequestService.requestHandler<
@@ -2104,7 +2101,7 @@ export class OCPP16IncomingRequestService extends OCPPIncomingRequestService<OCP
               await sendAndSetConnectorStatus(chargingStation, {
                 connectorId,
                 status: OCPP16ChargePointStatus.Unavailable,
-              } as OCPP16StatusNotificationRequest)
+              })
             }
           }
           transactionsStarted = false
index e58d40b95d98f6083afb2e1d6763617881ffe37c..d03c7678ca626ab3b0e2772f27ababa0d74a1b14 100644 (file)
@@ -103,7 +103,7 @@ export class OCPP16RequestService extends OCPPRequestService {
         const requestPayload =
           params?.rawPayload === true
             ? (commandParams as RequestType)
-            : this.buildRequestPayload<RequestType>(chargingStation, commandName, commandParams)
+            : this.buildRequestPayload(chargingStation, commandName, commandParams)
         const messageId = generateUUID()
         logger.debug(
           `${chargingStation.logPrefix()} ${moduleName}.requestHandler: Sending '${commandName}' request with message ID '${messageId}'`
@@ -114,7 +114,7 @@ export class OCPP16RequestService extends OCPPRequestService {
             await sendAndSetConnectorStatus(chargingStation, {
               connectorId: (commandParams as OCPP16StartTransactionRequest).connectorId,
               status: OCPP16ChargePointStatus.Preparing,
-            } as OCPP16StatusNotificationRequest)
+            })
             break
         }
         const response = (await this.sendMessage(
@@ -158,12 +158,11 @@ export class OCPP16RequestService extends OCPPRequestService {
    * @param commandParams - Optional parameters provided by the caller for payload construction
    * @returns The fully constructed and validated request payload ready for transmission
    */
-  // eslint-disable-next-line @typescript-eslint/no-unnecessary-type-parameters
-  private buildRequestPayload<Request extends JsonType>(
+  private buildRequestPayload (
     chargingStation: ChargingStation,
     commandName: OCPP16RequestCommand,
     commandParams?: JsonType
-  ): Request {
+  ): JsonType {
     let connectorId: number | undefined
     let energyActiveImportRegister: number
     logger.debug(
@@ -175,9 +174,9 @@ export class OCPP16RequestService extends OCPPRequestService {
       case OCPP16RequestCommand.DIAGNOSTICS_STATUS_NOTIFICATION:
       case OCPP16RequestCommand.FIRMWARE_STATUS_NOTIFICATION:
       case OCPP16RequestCommand.METER_VALUES:
-        return commandParams as unknown as Request
+        return commandParams ?? OCPP16Constants.OCPP_REQUEST_EMPTY
       case OCPP16RequestCommand.HEARTBEAT:
-        return OCPP16Constants.OCPP_REQUEST_EMPTY as unknown as Request
+        return OCPP16Constants.OCPP_REQUEST_EMPTY
     }
     assertIsJsonObject(
       commandParams,
@@ -193,7 +192,7 @@ export class OCPP16RequestService extends OCPPRequestService {
         return {
           idTag: OCPP16Constants.OCPP_DEFAULT_IDTAG,
           ...params,
-        } as unknown as Request
+        }
       case OCPP16RequestCommand.START_TRANSACTION:
         return {
           idTag: OCPP16Constants.OCPP_DEFAULT_IDTAG,
@@ -215,12 +214,12 @@ export class OCPP16RequestService extends OCPPRequestService {
             )?.reservationId,
           }),
           ...params,
-        } as unknown as Request
+        }
       case OCPP16RequestCommand.STATUS_NOTIFICATION:
         return OCPP16ServiceUtils.buildStatusNotificationRequest({
           errorCode: ChargePointErrorCode.NO_ERROR,
           ...params,
-        } as OCPP16StatusNotificationRequest) as unknown as Request
+        } as OCPP16StatusNotificationRequest)
       case OCPP16RequestCommand.STOP_TRANSACTION:
         ;(chargingStation.stationInfo?.transactionDataMeterValues === true ||
           OCPP16ServiceUtils.isSigningEnabled(chargingStation)) &&
@@ -272,7 +271,7 @@ export class OCPP16RequestService extends OCPPRequestService {
             timestamp: new Date(),
             ...(transactionData != null && { transactionData }),
             ...params,
-          } as unknown as Request
+          }
         }
       default: {
         // OCPPError usage here is debatable: it's an error in the OCPP stack but not targeted to sendError().
index 3a6fa38470775014f5a46e15daf9b7146cd9cafd..61d5e8eebfd85a2e9ac262098a9c1d353b76e693 100644 (file)
@@ -26,7 +26,6 @@ import {
   OCPP16StandardParametersKey,
   type OCPP16StartTransactionRequest,
   type OCPP16StartTransactionResponse,
-  type OCPP16StatusNotificationRequest,
   type OCPP16StopTransactionRequest,
   type OCPP16StopTransactionResponse,
   OCPPVersion,
@@ -503,7 +502,7 @@ export class OCPP16ResponseService extends OCPPResponseService {
       await sendAndSetConnectorStatus(chargingStation, {
         connectorId,
         status: OCPP16ChargePointStatus.Charging,
-      } as OCPP16StatusNotificationRequest)
+      })
       logger.info(
         `${chargingStation.logPrefix()} ${moduleName}.handleResponseStartTransaction: Transaction with id ${payload.transactionId.toString()} STARTED on ${
           // eslint-disable-next-line @typescript-eslint/restrict-template-expressions
@@ -589,7 +588,7 @@ export class OCPP16ResponseService extends OCPPResponseService {
         await sendAndSetConnectorStatus(chargingStation, {
           connectorId: transactionConnectorId,
           status: OCPP16ChargePointStatus.Finishing,
-        } as OCPP16StatusNotificationRequest)
+        })
       }
       OCPP16ServiceUtils.stopUpdatedMeterValues(chargingStation, transactionConnectorId)
       if (transactionConnectorStatus != null) {
index 1b137cf0ec78ffb8b4c7b0da101509667f9fb49c..ba7619b41cde02db44c4deb782d0cae7d550f956 100644 (file)
@@ -365,7 +365,7 @@ export class OCPP16ServiceUtils {
         await sendAndSetConnectorStatus(chargingStation, {
           connectorId,
           status: chargePointStatus,
-        } as OCPP16StatusNotificationRequest)
+        })
       }
       responses.push(response)
     }
@@ -791,7 +791,7 @@ export class OCPP16ServiceUtils {
     await sendAndSetConnectorStatus(chargingStation, {
       connectorId,
       status: OCPP16ChargePointStatus.Finishing,
-    } as OCPP16StatusNotificationRequest)
+    })
     const stopResponse = await OCPP16ServiceUtils.stopTransactionOnConnector(
       chargingStation,
       connectorId,
@@ -954,7 +954,7 @@ export class OCPP16ServiceUtils {
       await sendAndSetConnectorStatus(chargingStation, {
         connectorId,
         status: OCPP16ChargePointStatus.Finishing,
-      } as OCPP16StatusNotificationRequest)
+      })
     }
     const rawTransactionId = chargingStation.getConnectorStatus(connectorId)?.transactionId
     const transactionId = rawTransactionId != null ? convertToInt(rawTransactionId) : undefined
index 0d614d3292f3504a52421d6d7c3ddc33b6022685..8a60e568b2c4995632748abf60953041f169c4d9 100644 (file)
@@ -107,7 +107,6 @@ import {
   type OCPP20SetNetworkProfileResponse,
   type OCPP20SetVariablesRequest,
   type OCPP20SetVariablesResponse,
-  type OCPP20StatusNotificationRequest,
   type OCPP20StatusNotificationResponse,
   OCPP20TransactionEventEnumType,
   type OCPP20TriggerMessageRequest,
@@ -129,6 +128,7 @@ import {
   ResetStatusEnumType,
   SetNetworkProfileStatusEnumType,
   SetVariableStatusEnumType,
+  type StatusNotificationOptions,
   StopTransactionReason,
   TriggerMessageStatusEnumType,
   UnlockStatusEnumType,
@@ -185,7 +185,7 @@ import {
 } from './OCPP20CertificateManager.js'
 import { OCPP20CertSigningRetryManager } from './OCPP20CertSigningRetryManager.js'
 import { OCPP20Constants } from './OCPP20Constants.js'
-import { OCPP20ServiceUtils } from './OCPP20ServiceUtils.js'
+import { isOCPP20ConnectorStatus, OCPP20ServiceUtils } from './OCPP20ServiceUtils.js'
 import { OCPP20VariableManager } from './OCPP20VariableManager.js'
 import { getVariableMetadata, VARIABLE_REGISTRY } from './OCPP20VariableRegistry.js'
 
@@ -198,6 +198,21 @@ const moduleName = 'OCPP20IncomingRequestService'
 // device capability declarations with no spec-defined ceiling.
 const MAX_ITEMS_PER_REPORT_MESSAGE = 100 as const
 
+// V2GCertificateChain has no InstallCertificateUse counterpart, so it collapses onto V2GRootCertificate.
+const getCertificateIdUseToInstallCertificateUse: Readonly<
+  Record<GetCertificateIdUseEnumType, InstallCertificateUseEnumType>
+> = Object.freeze({
+  [GetCertificateIdUseEnumType.CSMSRootCertificate]:
+    InstallCertificateUseEnumType.CSMSRootCertificate,
+  [GetCertificateIdUseEnumType.ManufacturerRootCertificate]:
+    InstallCertificateUseEnumType.ManufacturerRootCertificate,
+  [GetCertificateIdUseEnumType.MORootCertificate]: InstallCertificateUseEnumType.MORootCertificate,
+  [GetCertificateIdUseEnumType.V2GCertificateChain]:
+    InstallCertificateUseEnumType.V2GRootCertificate,
+  [GetCertificateIdUseEnumType.V2GRootCertificate]:
+    InstallCertificateUseEnumType.V2GRootCertificate,
+})
+
 interface StationInfoReportField {
   property: 'chargePointModel' | 'chargePointSerialNumber' | 'chargePointVendor' | 'firmwareVersion'
   variable: OCPP20DeviceInfoVariableName
@@ -1580,7 +1595,7 @@ export class OCPP20IncomingRequestService extends OCPPIncomingRequestService<OCP
     sendAndSetConnectorStatus(chargingStation, {
       connectorId,
       connectorStatus: resolvedStatus,
-    } as unknown as OCPP20StatusNotificationRequest).catch((error: unknown) => {
+    }).catch((error: unknown) => {
       logger.error(
         `${chargingStation.logPrefix()} ${moduleName}.handleConnectorChangeAvailability: Error sending status notification for connector ${connectorId.toString()}:`,
         error
@@ -2163,12 +2178,7 @@ export class OCPP20IncomingRequestService extends OCPPIncomingRequestService<OCP
     }
 
     try {
-      const filterTypes = certificateType?.map(ct => {
-        if (ct === GetCertificateIdUseEnumType.V2GCertificateChain) {
-          return InstallCertificateUseEnumType.V2GRootCertificate
-        }
-        return ct as unknown as InstallCertificateUseEnumType
-      })
+      const filterTypes = certificateType?.map(ct => getCertificateIdUseToInstallCertificateUse[ct])
 
       const methodResult = chargingStation.certificateManager.getInstalledCertificates(
         chargingStation.stationInfo?.hashId ?? '',
@@ -3185,7 +3195,7 @@ export class OCPP20IncomingRequestService extends OCPPIncomingRequestService<OCP
         connectorId,
         connectorStatus: ConnectorStatusEnum.Available,
         evseId,
-      } as unknown as OCPP20StatusNotificationRequest)
+      })
 
       chargingStation.unlockConnector(connectorId)
       return { status: UnlockStatusEnumType.Unlocked }
@@ -3470,12 +3480,10 @@ export class OCPP20IncomingRequestService extends OCPPIncomingRequestService<OCP
         for (const [connectorId, connector] of evseStatus.connectors) {
           if (
             connector.status != null &&
+            isOCPP20ConnectorStatus(connector.status) &&
             !stationState.preInoperativeConnectorStatuses.has(connectorId)
           ) {
-            stationState.preInoperativeConnectorStatuses.set(
-              connectorId,
-              connector.status as unknown as OCPP20ConnectorStatusEnumType
-            )
+            stationState.preInoperativeConnectorStatuses.set(connectorId, connector.status)
           }
         }
       }
@@ -3594,7 +3602,7 @@ export class OCPP20IncomingRequestService extends OCPPIncomingRequestService<OCP
       sendAndSetConnectorStatus(chargingStation, {
         connectorId,
         connectorStatus: status,
-      } as unknown as OCPP20StatusNotificationRequest).catch((error: unknown) => {
+      }).catch((error: unknown) => {
         logger.error(
           `${chargingStation.logPrefix()} ${moduleName}.sendAllConnectorsStatusNotifications: Error sending status notification for connector ${connectorId.toString()}:`,
           error
@@ -3620,7 +3628,7 @@ export class OCPP20IncomingRequestService extends OCPPIncomingRequestService<OCP
         sendAndSetConnectorStatus(chargingStation, {
           connectorId,
           connectorStatus: status,
-        } as unknown as OCPP20StatusNotificationRequest).catch((error: unknown) => {
+        }).catch((error: unknown) => {
           logger.error(
             `${chargingStation.logPrefix()} ${moduleName}.sendEvseStatusNotifications: Error sending status notification for connector ${connectorId.toString()}:`,
             error
@@ -3847,7 +3855,7 @@ export class OCPP20IncomingRequestService extends OCPPIncomingRequestService<OCP
       sendAndSetConnectorStatus(chargingStation, {
         connectorId,
         connectorStatus: restoredStatus,
-      } as unknown as OCPP20StatusNotificationRequest).catch((error: unknown) => {
+      }).catch((error: unknown) => {
         logger.error(
           `${chargingStation.logPrefix()} ${moduleName}.sendRestoredAllConnectorsStatusNotifications: Error sending status notification for connector ${connectorId.toString()}:`,
           error
@@ -3867,7 +3875,7 @@ export class OCPP20IncomingRequestService extends OCPPIncomingRequestService<OCP
         sendAndSetConnectorStatus(chargingStation, {
           connectorId,
           connectorStatus: restoredStatus,
-        } as unknown as OCPP20StatusNotificationRequest).catch((error: unknown) => {
+        }).catch((error: unknown) => {
           logger.error(
             `${chargingStation.logPrefix()} ${moduleName}.sendRestoredEvseStatusNotifications: Error sending status notification for connector ${connectorId.toString()}:`,
             error
@@ -4247,14 +4255,14 @@ export class OCPP20IncomingRequestService extends OCPPIncomingRequestService<OCP
     )) {
       const resolvedStatus = connectorStatus.status ?? ConnectorStatusEnum.Available
       chargingStation.ocppRequestService
-        .requestHandler<OCPP20StatusNotificationRequest, OCPP20StatusNotificationResponse>(
+        .requestHandler<StatusNotificationOptions, OCPP20StatusNotificationResponse>(
           chargingStation,
           OCPP20RequestCommand.STATUS_NOTIFICATION,
           {
             connectorId,
             connectorStatus: resolvedStatus,
             evseId,
-          } as unknown as OCPP20StatusNotificationRequest,
+          },
           { skipBufferingOnError: true, triggerMessage: true }
         )
         .catch(errorHandler)
@@ -4320,14 +4328,14 @@ export class OCPP20IncomingRequestService extends OCPPIncomingRequestService<OCP
       const connectorStatus = evseStatus?.connectors.get(evse.connectorId)
       const resolvedStatus = connectorStatus?.status ?? ConnectorStatusEnum.Available
       chargingStation.ocppRequestService
-        .requestHandler<OCPP20StatusNotificationRequest, OCPP20StatusNotificationResponse>(
+        .requestHandler<StatusNotificationOptions, OCPP20StatusNotificationResponse>(
           chargingStation,
           OCPP20RequestCommand.STATUS_NOTIFICATION,
           {
             connectorId: evse.connectorId,
             connectorStatus: resolvedStatus,
             evseId: evse.id,
-          } as unknown as OCPP20StatusNotificationRequest,
+          },
           { skipBufferingOnError: true, triggerMessage: true }
         )
         .catch(errorHandler)
index 381f75b462265b590219cb0fc5659f7e8d3079c7..01ef06b62781a52b63498f527359cdd57c3bde4c 100644 (file)
@@ -14,10 +14,10 @@ import {
   OCPP20RequestCommand,
   OCPP20RequiredVariableName,
   type OCPP20SignCertificateRequest,
-  type OCPP20StatusNotificationRequest,
   type OCPP20TransactionEventOptions,
   OCPPVersion,
   type RequestParams,
+  type StatusNotificationOptions,
 } from '../../../types/index.js'
 import { generateUUID, getErrorMessage, logger } from '../../../utils/index.js'
 import { OCPPRequestService } from '../OCPPRequestService.js'
@@ -28,6 +28,10 @@ import { buildTransactionEvent, OCPP20ServiceUtils } from './OCPP20ServiceUtils.
 
 const moduleName = 'OCPP20RequestService'
 
+interface SignCertificateOptions extends JsonObject {
+  certificateType?: CertificateSigningUseEnumType
+}
+
 /**
  * OCPP 2.0.1 Request Service
  *
@@ -112,7 +116,7 @@ export class OCPP20RequestService extends OCPPRequestService {
         const requestPayload =
           params?.rawPayload === true
             ? (commandParams as RequestType)
-            : this.buildRequestPayload<RequestType>(chargingStation, commandName, commandParams)
+            : this.buildRequestPayload(chargingStation, commandName, commandParams)
         const messageId = generateUUID()
         logger.debug(
           `${chargingStation.logPrefix()} ${moduleName}.requestHandler: Sending '${commandName}' request with message ID '${messageId}'`
@@ -139,12 +143,11 @@ export class OCPP20RequestService extends OCPPRequestService {
     throw new OCPPError(ErrorType.NOT_SUPPORTED, errorMsg, commandName, commandParams)
   }
 
-  // eslint-disable-next-line @typescript-eslint/no-unnecessary-type-parameters
-  private buildRequestPayload<Request extends JsonType>(
+  private buildRequestPayload (
     chargingStation: ChargingStation,
     commandName: OCPP20RequestCommand,
     commandParams?: JsonType
-  ): Request {
+  ): JsonType {
     logger.debug(
       `${chargingStation.logPrefix()} ${moduleName}.buildRequestPayload: Building '${commandName}' payload`
     )
@@ -160,9 +163,9 @@ export class OCPP20RequestService extends OCPPRequestService {
       case OCPP20RequestCommand.NOTIFY_CUSTOMER_INFORMATION:
       case OCPP20RequestCommand.NOTIFY_REPORT:
       case OCPP20RequestCommand.SECURITY_EVENT_NOTIFICATION:
-        return commandParams as unknown as Request
+        return commandParams ?? OCPP20Constants.OCPP_REQUEST_EMPTY
       case OCPP20RequestCommand.HEARTBEAT:
-        return OCPP20Constants.OCPP_RESPONSE_EMPTY as unknown as Request
+        return OCPP20Constants.OCPP_REQUEST_EMPTY
       case OCPP20RequestCommand.SIGN_CERTIFICATE: {
         let csr: string
         try {
@@ -186,26 +189,26 @@ export class OCPP20RequestService extends OCPPRequestService {
           )
         }
 
-        const certificateType = (commandParams as JsonObject | undefined)?.certificateType as
-          CertificateSigningUseEnumType | undefined
+        const certificateType = (commandParams as SignCertificateOptions | undefined)
+          ?.certificateType
 
         const requestPayload: OCPP20SignCertificateRequest = {
           csr,
           ...(certificateType != null && { certificateType }),
         }
 
-        return requestPayload as unknown as Request
+        return requestPayload
       }
       case OCPP20RequestCommand.STATUS_NOTIFICATION:
         return OCPP20ServiceUtils.buildStatusNotificationRequest(
           chargingStation,
-          commandParams as unknown as OCPP20StatusNotificationRequest
-        ) as unknown as Request
+          commandParams as StatusNotificationOptions
+        )
       case OCPP20RequestCommand.TRANSACTION_EVENT:
         return buildTransactionEvent(
           chargingStation,
-          commandParams as unknown as OCPP20TransactionEventOptions
-        ) as unknown as Request
+          commandParams as OCPP20TransactionEventOptions
+        )
       default: {
         // OCPPError usage here is debatable: it's an error in the OCPP stack but not targeted to sendError().
         const errorMsg = `Unsupported OCPP command ${commandName as string} for payload building`
index 9ef4d0b0e641093592620ec788c68a3686ed14d8..bf4c6791834a958e4408255e81d42235c1db85e2 100644 (file)
@@ -30,7 +30,6 @@ import {
   type OCPP20SecurityEventNotificationResponse,
   type OCPP20SignCertificateRequest,
   type OCPP20SignCertificateResponse,
-  type OCPP20StatusNotificationRequest,
   type OCPP20StatusNotificationResponse,
   OCPP20TransactionEventEnumType,
   type OCPP20TransactionEventRequest,
@@ -444,7 +443,7 @@ export class OCPP20ResponseService extends OCPPResponseService {
             sendAndSetConnectorStatus(chargingStation, {
               connectorId,
               connectorStatus: ConnectorStatusEnum.Occupied,
-            } as unknown as OCPP20StatusNotificationRequest).catch((error: unknown) => {
+            }).catch((error: unknown) => {
               logger.error(
                 `${chargingStation.logPrefix()} ${moduleName}.handleResponseTransactionEvent: Error sending StatusNotification(Occupied):`,
                 error
index 1364d9fa14a15d2a25726e0437cda036e1f179b9..dafd64b89a9e5d2d7e0dcd174688764a2bd2b336 100644 (file)
@@ -10,7 +10,7 @@ import {
   OCPP20AuthorizationStatusEnumType,
   OCPP20ChargingStateEnumType,
   OCPP20ComponentName,
-  type OCPP20ConnectorStatusEnumType,
+  OCPP20ConnectorStatusEnumType,
   type OCPP20EVSEType,
   type OCPP20GetVariableResultType,
   OCPP20IdTokenEnumType,
@@ -34,6 +34,7 @@ import {
   ReasonCodeEnumType,
   RequestCommand,
   type StartTransactionResult,
+  type StatusNotificationOptions,
   type StopTransactionReason,
   type StopTransactionResult,
   type UUIDv4,
@@ -71,6 +72,11 @@ import { OCPP20VariableManager } from './OCPP20VariableManager.js'
 
 const moduleName = 'OCPP20ServiceUtils'
 
+export const isOCPP20ConnectorStatus = (
+  status: ConnectorStatusEnum
+): status is OCPP20ConnectorStatusEnumType =>
+  Object.values(OCPP20ConnectorStatusEnumType).some(value => value === status)
+
 export interface RejectionReason {
   additionalInfo: string
   reasonCode: ReasonCodeEnumType
@@ -126,17 +132,16 @@ export class OCPP20ServiceUtils {
 
   /**
    * @param chargingStation - Target charging station for EVSE resolution
-   * @param commandParams - Status notification parameters
+   * @param commandParams - StatusNotification input; `connectorStatus` takes precedence over `status`
    * @returns Formatted OCPP 2.0.1 StatusNotification request payload
+   * @throws {OCPPError} When the EVSE id cannot be resolved or the connector status is missing/not a valid OCPP 2.0.1 status
    */
   public static buildStatusNotificationRequest (
     chargingStation: ChargingStation,
-    commandParams: OCPP20StatusNotificationRequest
+    commandParams: StatusNotificationOptions
   ): OCPP20StatusNotificationRequest {
-    const params = commandParams as Record<string, unknown>
-    const connectorId = params.connectorId as number
-    const connectorStatus = (params.connectorStatus ?? params.status) as ConnectorStatusEnum
-    const evseId = params.evseId as number | undefined
+    const { connectorId, evseId } = commandParams
+    const connectorStatus = commandParams.connectorStatus ?? commandParams.status
     const resolvedEvseId = evseId ?? chargingStation.getEvseIdByConnectorId(connectorId)
     if (resolvedEvseId === undefined) {
       throw new OCPPError(
@@ -145,9 +150,16 @@ export class OCPP20ServiceUtils {
         RequestCommand.STATUS_NOTIFICATION
       )
     }
+    if (connectorStatus == null || !isOCPP20ConnectorStatus(connectorStatus)) {
+      throw new OCPPError(
+        ErrorType.INTERNAL_ERROR,
+        `Cannot build status notification payload: invalid connector status for connector ${connectorId.toString()}`,
+        RequestCommand.STATUS_NOTIFICATION
+      )
+    }
     return {
       connectorId,
-      connectorStatus: connectorStatus as OCPP20ConnectorStatusEnumType,
+      connectorStatus,
       evseId: resolvedEvseId,
       timestamp: new Date(),
     } satisfies OCPP20StatusNotificationRequest
@@ -813,7 +825,7 @@ export class OCPP20ServiceUtils {
       )
 
       const response = await chargingStation.ocppRequestService.requestHandler<
-        OCPP20TransactionEventRequest,
+        OCPP20TransactionEventOptions,
         OCPP20TransactionEventResponse
       >(chargingStation, OCPP20RequestCommand.TRANSACTION_EVENT, {
         connectorId,
@@ -821,7 +833,7 @@ export class OCPP20ServiceUtils {
         transactionId,
         triggerReason,
         ...options,
-      } as unknown as OCPP20TransactionEventRequest)
+      })
 
       return response
     } catch (error) {
index 2ad010ebdd5e6f10b9819acdf644e7a838576fda..68c7f80a15eca299aad929f8caa1cda0f1b72e75 100644 (file)
@@ -5,10 +5,9 @@ import {
   type ConnectorStatus,
   ConnectorStatusEnum,
   ErrorType,
-  type OCPP16ChargePointErrorCode,
   OCPPVersion,
   RequestCommand,
-  type StatusNotificationRequest,
+  type StatusNotificationOptions,
   type StatusNotificationResponse,
 } from '../../types/index.js'
 import { logger } from '../../utils/index.js'
@@ -18,20 +17,18 @@ import { OCPP20Constants } from './2.0/OCPP20Constants.js'
 /**
  * Sends a StatusNotification request and updates the connector status locally.
  * @param chargingStation - Target charging station
- * @param commandParams - Status notification parameters including connector ID and status
+ * @param commandParams - Cross-version StatusNotification input; `connectorStatus` (OCPP 2.0.1) takes precedence over `status` (OCPP 1.6)
  * @param options - Optional settings to control whether the request is actually sent
  * @param options.send - Whether to actually send the status notification
  */
 export const sendAndSetConnectorStatus = async (
   chargingStation: ChargingStation,
-  commandParams: StatusNotificationRequest,
+  commandParams: StatusNotificationOptions,
   options?: { send: boolean }
 ): Promise<void> => {
   options = { send: true, ...options }
-  const params = commandParams as Record<string, unknown>
-  const connectorId = params.connectorId as number
-  const status = (params.connectorStatus ?? params.status) as ConnectorStatusEnum
-  const errorCode = params.errorCode as OCPP16ChargePointErrorCode | undefined
+  const { connectorId, errorCode } = commandParams
+  const status = commandParams.connectorStatus ?? commandParams.status
   const connectorStatus = chargingStation.getConnectorStatus(connectorId)
   if (connectorStatus == null) {
     return
@@ -39,7 +36,7 @@ export const sendAndSetConnectorStatus = async (
   if (options.send) {
     checkConnectorStatusTransition(chargingStation, connectorId, status)
     await chargingStation.ocppRequestService.requestHandler<
-      StatusNotificationRequest,
+      StatusNotificationOptions,
       StatusNotificationResponse
     >(chargingStation, RequestCommand.STATUS_NOTIFICATION, commandParams)
   }
@@ -70,7 +67,7 @@ export const sendPostTransactionStatus = async (
     connectorId,
     connectorStatus: status,
     status,
-  } as unknown as StatusNotificationRequest)
+  })
 }
 
 /**
@@ -91,19 +88,19 @@ export const restoreConnectorStatus = async (
     await sendAndSetConnectorStatus(chargingStation, {
       connectorId,
       status: ConnectorStatusEnum.Reserved,
-    } as unknown as StatusNotificationRequest)
+    })
   } else if (connectorStatus?.status !== ConnectorStatusEnum.Available) {
     await sendAndSetConnectorStatus(chargingStation, {
       connectorId,
       status: ConnectorStatusEnum.Available,
-    } as unknown as StatusNotificationRequest)
+    })
   }
 }
 
 const checkConnectorStatusTransition = (
   chargingStation: ChargingStation,
   connectorId: number,
-  status: ConnectorStatusEnum
+  status: ConnectorStatusEnum | undefined
 ): boolean => {
   const fromStatus = chargingStation.getConnectorStatus(connectorId)?.status
   let chargingStationTransitions: readonly { from?: ConnectorStatusEnum; to: ConnectorStatusEnum }[]
@@ -137,7 +134,10 @@ const checkConnectorStatusTransition = (
       } connector id ${connectorId.toString()} status transition from '${
         // eslint-disable-next-line @typescript-eslint/restrict-template-expressions
         chargingStation.getConnectorStatus(connectorId)?.status
-      }' to '${status}' is not allowed`
+      }' to '${
+        // eslint-disable-next-line @typescript-eslint/restrict-template-expressions
+        status
+      }' is not allowed`
     )
   }
   return transitionAllowed
index 95104f5641593793dbf2e0f5ce42e86a92bc1a8b..55a5319ac9be9fcf496183f9d818b422213df789 100644 (file)
@@ -291,11 +291,11 @@ export abstract class OCPPIncomingRequestService<
    */
   protected abstract resetStationState (stationState: TStationState): void
 
-  // eslint-disable-next-line @typescript-eslint/no-unnecessary-type-parameters -- bridges contravariant handler signatures into IncomingRequestHandler
-  protected toRequestHandler<P extends JsonType, R extends JsonType>(
-    handler: (chargingStation: ChargingStation, commandPayload: P) => Promise<R> | R
+  // eslint-disable-next-line @typescript-eslint/no-unnecessary-type-parameters -- P bridges contravariant handler payload signatures into IncomingRequestHandler
+  protected toRequestHandler<P extends JsonType>(
+    handler: (chargingStation: ChargingStation, commandPayload: P) => JsonType | Promise<JsonType>
   ): IncomingRequestHandler {
-    return handler as unknown as IncomingRequestHandler
+    return handler as IncomingRequestHandler
   }
 
   /**
index 34d6c0ea049b132ea484ef3cff62cf94343d592e..056ac1c2d1772f97fca494235cf5199dd4561fe0 100644 (file)
@@ -131,7 +131,7 @@ export abstract class OCPPResponseService {
       requestPayload: R
     ) => Promise<void> | void
   ): ResponseHandler {
-    return handler as unknown as ResponseHandler
+    return handler as ResponseHandler
   }
 
   /**
index c3ae97664e93d7d4a13b8854098abe1dbe3e1d54..8e102f3c0bfc414d11d246ba124c3f96861aaf3d 100644 (file)
@@ -331,7 +331,7 @@ export const isOCPP16Type = (type: IdentifierType): boolean => {
  * @returns True if OCPP 2.0.1 type
  */
 export const isOCPP20Type = (type: IdentifierType): boolean => {
-  return Object.values(OCPP20IdTokenEnumType).includes(type as unknown as OCPP20IdTokenEnumType)
+  return (Object.values(OCPP20IdTokenEnumType) as string[]).includes(type)
 }
 
 /**
index 5dd88ef5ba4e7346208617aa9a58c68d381a5269..bfb436ee54113126e6ab7bc0b84e7049c61ac06d 100644 (file)
@@ -394,6 +394,7 @@ export {
   type RequestParams,
   type ResponseCallback,
   type ResponseType,
+  type StatusNotificationOptions,
   type StatusNotificationRequest,
 } from './ocpp/Requests.js'
 export {
index 1992c88d7ace521cbba8496f5480de1ce64faa05..daed85f6429791183a8cffb7325d3f668b7dfdf9 100644 (file)
@@ -271,7 +271,7 @@ export interface OCPP20MessageContentType extends JsonObject {
  * Optional parameters for building and sending TransactionEvent requests.
  * Aligned with OCPP 2.0.1 TransactionEvent.req optional fields.
  */
-export interface OCPP20TransactionEventOptions {
+export interface OCPP20TransactionEventOptions extends JsonObject {
   cableMaxCurrent?: number
   chargingState?: OCPP20ChargingStateEnumType
   connectorId?: number
index 28ddadbf02c053392dbe8d1cbdb48f892c798a71..5780ee7ded56ba889a6717f53769ce948a41a8a5 100644 (file)
@@ -1,8 +1,10 @@
 import type { ChargingStation } from '../../charging-station/index.js'
 import type { OCPPError } from '../../exception/index.js'
-import type { JsonType } from '../JsonType.js'
+import type { JsonObject, JsonType } from '../JsonType.js'
+import type { OCPP16ChargePointErrorCode } from './1.6/ChargePointErrorCode.js'
 import type { OCPP16MeterValuesRequest } from './1.6/MeterValues.js'
 import type { OCPP20MeterValuesRequest } from './2.0/MeterValues.js'
+import type { ConnectorStatusEnum } from './ConnectorStatusEnum.js'
 import type { MessageType } from './MessageType.js'
 
 import { OCPP16DiagnosticsStatus } from './1.6/DiagnosticsStatus.js'
@@ -98,6 +100,22 @@ export type MeterValuesRequest = OCPP16MeterValuesRequest | OCPP20MeterValuesReq
 
 export type ResponseCallback = (payload: JsonType, requestPayload: JsonType) => void
 
+/**
+ * Cross-version (OCPP 1.6/2.0.1) input for a StatusNotification request, looser than the per-version
+ * wire types it is built into. `connectorStatus` (2.0.1) takes precedence over `status` (1.6).
+ */
+export interface StatusNotificationOptions extends JsonObject {
+  connectorId: number
+  connectorStatus?: ConnectorStatusEnum
+  errorCode?: OCPP16ChargePointErrorCode
+  evseId?: number
+  info?: string
+  status?: ConnectorStatusEnum
+  timestamp?: Date
+  vendorErrorCode?: string
+  vendorId?: string
+}
+
 export type StatusNotificationRequest =
   OCPP16StatusNotificationRequest | OCPP20StatusNotificationRequest
 
index 32dcbd3fbc074f63d0e672aba867254142bbd856..8311961c8f339c879ff2f95965c914475040b836 100644 (file)
@@ -12,9 +12,11 @@ import type { ChargingStation } from '../../../../src/charging-station/index.js'
 import { OCPP20ServiceUtils } from '../../../../src/charging-station/ocpp/2.0/OCPP20ServiceUtils.js'
 import { OCPPError } from '../../../../src/exception/index.js'
 import {
+  OCPP16ChargePointStatus,
   OCPP20ConnectorStatusEnumType,
   type OCPP20StatusNotificationRequest,
   OCPPVersion,
+  type StatusNotificationOptions,
 } from '../../../../src/types/index.js'
 import { Constants } from '../../../../src/utils/index.js'
 import { standardCleanup } from '../../../helpers/TestLifecycleHelpers.js'
@@ -141,5 +143,40 @@ await describe('OCPP20ServiceUtils', async () => {
 
       assert.strictEqual(result.connectorStatus, OCPP20ConnectorStatusEnumType.Faulted)
     })
+
+    await it('should throw OCPPError when connectorStatus and status are both missing', () => {
+      const input: StatusNotificationOptions = {
+        connectorId: 1,
+        evseId: 1,
+      }
+
+      assert.throws(
+        () => {
+          OCPP20ServiceUtils.buildStatusNotificationRequest(mockStation, input)
+        },
+        (error: unknown) => {
+          assert.ok(error instanceof OCPPError)
+          return true
+        }
+      )
+    })
+
+    await it('should throw OCPPError when connectorStatus is not a valid OCPP 2.0.1 status', () => {
+      const input: StatusNotificationOptions = {
+        connectorId: 1,
+        connectorStatus: OCPP16ChargePointStatus.Preparing,
+        evseId: 1,
+      }
+
+      assert.throws(
+        () => {
+          OCPP20ServiceUtils.buildStatusNotificationRequest(mockStation, input)
+        },
+        (error: unknown) => {
+          assert.ok(error instanceof OCPPError)
+          return true
+        }
+      )
+    })
   })
 })
index d6cebe703895ed5b61f536ee1e07678c2bb92768..5177612fa3787117d661f1b5228a53b77c1d9ea3 100644 (file)
@@ -16,12 +16,7 @@ import {
   restoreConnectorStatus,
   sendAndSetConnectorStatus,
 } from '../../../src/charging-station/ocpp/OCPPConnectorStatusOperations.js'
-import {
-  ConnectorStatusEnum,
-  type OCPP16StatusNotificationRequest,
-  type OCPP20StatusNotificationRequest,
-  OCPPVersion,
-} from '../../../src/types/index.js'
+import { ConnectorStatusEnum, OCPPVersion } from '../../../src/types/index.js'
 import {
   createStationWithRequestHandler,
   standardCleanup,
@@ -40,7 +35,7 @@ await describe('OCPPConnectorStatusOperations', async () => {
       await sendAndSetConnectorStatus(station, {
         connectorId: 1,
         status: ConnectorStatusEnum.Occupied,
-      } as unknown as OCPP16StatusNotificationRequest)
+      })
 
       assert.strictEqual(requestHandler.mock.calls.length, 1)
       assert.strictEqual(station.getConnectorStatus(1)?.status, ConnectorStatusEnum.Occupied)
@@ -52,7 +47,7 @@ await describe('OCPPConnectorStatusOperations', async () => {
       await sendAndSetConnectorStatus(station, {
         connectorId: 99,
         status: ConnectorStatusEnum.Occupied,
-      } as unknown as OCPP16StatusNotificationRequest)
+      })
 
       assert.strictEqual(requestHandler.mock.calls.length, 0)
     })
@@ -65,7 +60,7 @@ await describe('OCPPConnectorStatusOperations', async () => {
         {
           connectorId: 1,
           status: ConnectorStatusEnum.Occupied,
-        } as unknown as OCPP16StatusNotificationRequest,
+        },
         {
           send: false,
         }
@@ -83,7 +78,7 @@ await describe('OCPPConnectorStatusOperations', async () => {
       await sendAndSetConnectorStatus(station, {
         connectorId: 1,
         status: ConnectorStatusEnum.Unavailable,
-      } as unknown as OCPP16StatusNotificationRequest)
+      })
 
       assert.strictEqual(station.getConnectorStatus(1)?.status, ConnectorStatusEnum.Unavailable)
     })
@@ -96,7 +91,7 @@ await describe('OCPPConnectorStatusOperations', async () => {
       await sendAndSetConnectorStatus(station, {
         connectorId: 1,
         status: ConnectorStatusEnum.Occupied,
-      } as unknown as OCPP16StatusNotificationRequest)
+      })
 
       assert.strictEqual(emitSpy.mock.calls.length, 1)
     })
@@ -110,7 +105,7 @@ await describe('OCPPConnectorStatusOperations', async () => {
         connectorId: 1,
         connectorStatus: ConnectorStatusEnum.Occupied,
         evseId: 1,
-      } as unknown as OCPP20StatusNotificationRequest)
+      })
 
       assert.strictEqual(requestHandler.mock.calls.length, 1)
       assert.strictEqual(station.getConnectorStatus(1)?.status, ConnectorStatusEnum.Occupied)
@@ -122,7 +117,7 @@ await describe('OCPPConnectorStatusOperations', async () => {
       await sendAndSetConnectorStatus(station, {
         connectorId: 1,
         status: ConnectorStatusEnum.Occupied,
-      } as unknown as OCPP16StatusNotificationRequest)
+      })
 
       assert.strictEqual(requestHandler.mock.calls.length, 1)
     })