Refine UI protocol documentation
[e-mobility-charging-stations-simulator.git] / src / charging-station / ocpp / 1.6 / OCPP16IncomingRequestService.ts
index d431b9a8a4def3a68b64af4e7313ab8af1c38700..c35b3e6f61e7a4e6c6e893dd78e2c9d812cb242a 100644 (file)
@@ -4,11 +4,12 @@ import fs from 'fs';
 import path from 'path';
 import { URL, fileURLToPath } from 'url';
 
+import type { JSONSchemaType } from 'ajv';
 import { Client, FTPResponse } from 'basic-ftp';
 import tar from 'tar';
 
 import OCPPError from '../../../exception/OCPPError';
-import { JsonType } from '../../../types/JsonType';
+import type { JsonObject, JsonType } from '../../../types/JsonType';
 import { OCPP16ChargePointErrorCode } from '../../../types/ocpp/1.6/ChargePointErrorCode';
 import { OCPP16ChargePointStatus } from '../../../types/ocpp/1.6/ChargePointStatus';
 import {
@@ -20,7 +21,7 @@ import {
   OCPP16SupportedFeatureProfiles,
 } from '../../../types/ocpp/1.6/Configuration';
 import { OCPP16DiagnosticsStatus } from '../../../types/ocpp/1.6/DiagnosticsStatus';
-import {
+import type {
   OCPP16MeterValuesRequest,
   OCPP16MeterValuesResponse,
 } from '../../../types/ocpp/1.6/MeterValues';
@@ -34,6 +35,7 @@ import {
   MessageTrigger,
   OCPP16AvailabilityType,
   OCPP16BootNotificationRequest,
+  OCPP16ClearCacheRequest,
   OCPP16HeartbeatRequest,
   OCPP16IncomingRequestCommand,
   OCPP16RequestCommand,
@@ -45,7 +47,7 @@ import {
   SetChargingProfileRequest,
   UnlockConnectorRequest,
 } from '../../../types/ocpp/1.6/Requests';
-import {
+import type {
   ChangeAvailabilityResponse,
   ChangeConfigurationResponse,
   ClearChargingProfileResponse,
@@ -69,10 +71,10 @@ import {
   OCPP16StopTransactionRequest,
   OCPP16StopTransactionResponse,
 } from '../../../types/ocpp/1.6/Transaction';
-import { OCPPConfigurationKey } from '../../../types/ocpp/Configuration';
+import type { OCPPConfigurationKey } from '../../../types/ocpp/Configuration';
 import { ErrorType } from '../../../types/ocpp/ErrorType';
-import { IncomingRequestHandler } from '../../../types/ocpp/Requests';
-import { DefaultResponse } from '../../../types/ocpp/Responses';
+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';
@@ -86,6 +88,7 @@ const moduleName = 'OCPP16IncomingRequestService';
 
 export default class OCPP16IncomingRequestService extends OCPPIncomingRequestService {
   private incomingRequestHandlers: Map<OCPP16IncomingRequestCommand, IncomingRequestHandler>;
+  private jsonSchemas: Map<OCPP16IncomingRequestCommand, JSONSchemaType<JsonObject>>;
 
   public constructor() {
     if (new.target?.name === moduleName) {
@@ -127,6 +130,153 @@ export default class OCPP16IncomingRequestService extends OCPPIncomingRequestSer
       [OCPP16IncomingRequestCommand.GET_DIAGNOSTICS, this.handleRequestGetDiagnostics.bind(this)],
       [OCPP16IncomingRequestCommand.TRIGGER_MESSAGE, this.handleRequestTriggerMessage.bind(this)],
     ]);
+    this.jsonSchemas = new Map<OCPP16IncomingRequestCommand, JSONSchemaType<JsonObject>>([
+      [
+        OCPP16IncomingRequestCommand.RESET,
+        JSON.parse(
+          fs.readFileSync(
+            path.resolve(
+              path.dirname(fileURLToPath(import.meta.url)),
+              '../../../assets/json-schemas/ocpp/1.6/Reset.json'
+            ),
+            'utf8'
+          )
+        ) as JSONSchemaType<ResetRequest>,
+      ],
+      [
+        OCPP16IncomingRequestCommand.CLEAR_CACHE,
+        JSON.parse(
+          fs.readFileSync(
+            path.resolve(
+              path.dirname(fileURLToPath(import.meta.url)),
+              '../../../assets/json-schemas/ocpp/1.6/ClearCache.json'
+            ),
+            'utf8'
+          )
+        ) as JSONSchemaType<OCPP16ClearCacheRequest>,
+      ],
+      [
+        OCPP16IncomingRequestCommand.UNLOCK_CONNECTOR,
+        JSON.parse(
+          fs.readFileSync(
+            path.resolve(
+              path.dirname(fileURLToPath(import.meta.url)),
+              '../../../assets/json-schemas/ocpp/1.6/UnlockConnector.json'
+            ),
+            'utf8'
+          )
+        ) as JSONSchemaType<UnlockConnectorRequest>,
+      ],
+      [
+        OCPP16IncomingRequestCommand.GET_CONFIGURATION,
+        JSON.parse(
+          fs.readFileSync(
+            path.resolve(
+              path.dirname(fileURLToPath(import.meta.url)),
+              '../../../assets/json-schemas/ocpp/1.6/GetConfiguration.json'
+            ),
+            'utf8'
+          )
+        ) as JSONSchemaType<GetConfigurationRequest>,
+      ],
+      [
+        OCPP16IncomingRequestCommand.CHANGE_CONFIGURATION,
+        JSON.parse(
+          fs.readFileSync(
+            path.resolve(
+              path.dirname(fileURLToPath(import.meta.url)),
+              '../../../assets/json-schemas/ocpp/1.6/ChangeConfiguration.json'
+            ),
+            'utf8'
+          )
+        ) as JSONSchemaType<ChangeConfigurationRequest>,
+      ],
+      [
+        OCPP16IncomingRequestCommand.GET_DIAGNOSTICS,
+        JSON.parse(
+          fs.readFileSync(
+            path.resolve(
+              path.dirname(fileURLToPath(import.meta.url)),
+              '../../../assets/json-schemas/ocpp/1.6/GetDiagnostics.json'
+            ),
+            'utf8'
+          )
+        ) as JSONSchemaType<GetDiagnosticsRequest>,
+      ],
+      [
+        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'
+          )
+        ) as JSONSchemaType<SetChargingProfileRequest>,
+      ],
+      [
+        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'
+          )
+        ) as JSONSchemaType<ClearChargingProfileRequest>,
+      ],
+      [
+        OCPP16IncomingRequestCommand.CHANGE_AVAILABILITY,
+        JSON.parse(
+          fs.readFileSync(
+            path.resolve(
+              path.dirname(fileURLToPath(import.meta.url)),
+              '../../../assets/json-schemas/ocpp/1.6/ChangeAvailability.json'
+            ),
+            'utf8'
+          )
+        ) as JSONSchemaType<ChangeAvailabilityRequest>,
+      ],
+      [
+        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'
+          )
+        ) as JSONSchemaType<RemoteStartTransactionRequest>,
+      ],
+      [
+        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'
+          )
+        ) as JSONSchemaType<RemoteStopTransactionRequest>,
+      ],
+      [
+        OCPP16IncomingRequestCommand.TRIGGER_MESSAGE,
+        JSON.parse(
+          fs.readFileSync(
+            path.resolve(
+              path.dirname(fileURLToPath(import.meta.url)),
+              '../../../assets/json-schemas/ocpp/1.6/TriggerMessage.json'
+            ),
+            'utf8'
+          )
+        ) as JSONSchemaType<OCPP16TriggerMessageRequest>,
+      ],
+    ]);
+    this.validatePayload.bind(this);
   }
 
   public async incomingRequestHandler(
@@ -144,7 +294,7 @@ export default class OCPP16IncomingRequestService extends OCPPIncomingRequestSer
     ) {
       throw new OCPPError(
         ErrorType.SECURITY_ERROR,
-        `${commandName} cannot be issued to handle request payload ${JSON.stringify(
+        `${commandName} cannot be issued to handle request PDU ${JSON.stringify(
           commandPayload,
           null,
           2
@@ -157,8 +307,12 @@ export default class OCPP16IncomingRequestService extends OCPPIncomingRequestSer
       chargingStation.isRegistered() ||
       (!chargingStation.getOcppStrictCompliance() && chargingStation.isInUnknownState())
     ) {
-      if (this.incomingRequestHandlers.has(commandName)) {
+      if (
+        this.incomingRequestHandlers.has(commandName) &&
+        ChargingStationUtils.isIncomingRequestCommandSupported(commandName, chargingStation)
+      ) {
         try {
+          this.validatePayload(chargingStation, commandName, commandPayload);
           // Call the method to build the response
           response = await this.incomingRequestHandlers.get(commandName)(
             chargingStation,
@@ -166,14 +320,17 @@ export default class OCPP16IncomingRequestService extends OCPPIncomingRequestSer
           );
         } catch (error) {
           // Log
-          logger.error(chargingStation.logPrefix() + ' Handle request error: %j', error);
+          logger.error(
+            `${chargingStation.logPrefix()} ${moduleName}.incomingRequestHandler: Handle incoming request error:`,
+            error
+          );
           throw error;
         }
       } else {
         // Throw exception
         throw new OCPPError(
           ErrorType.NOT_IMPLEMENTED,
-          `${commandName} is not implemented to handle request payload ${JSON.stringify(
+          `${commandName} is not implemented to handle request PDU ${JSON.stringify(
             commandPayload,
             null,
             2
@@ -185,7 +342,7 @@ export default class OCPP16IncomingRequestService extends OCPPIncomingRequestSer
     } else {
       throw new OCPPError(
         ErrorType.SECURITY_ERROR,
-        `${commandName} cannot be issued to handle request payload ${JSON.stringify(
+        `${commandName} cannot be issued to handle request PDU ${JSON.stringify(
           commandPayload,
           null,
           2
@@ -203,6 +360,25 @@ export default class OCPP16IncomingRequestService extends OCPPIncomingRequestSer
     );
   }
 
+  private validatePayload(
+    chargingStation: ChargingStation,
+    commandName: OCPP16IncomingRequestCommand,
+    commandPayload: JsonType
+  ): boolean {
+    if (this.jsonSchemas.has(commandName)) {
+      return this.validateIncomingRequestPayload(
+        chargingStation,
+        commandName,
+        this.jsonSchemas.get(commandName),
+        commandPayload
+      );
+    }
+    logger.warn(
+      `${chargingStation.logPrefix()} ${moduleName}.validatePayload: No JSON schema found for command ${commandName} PDU validation`
+    );
+    return false;
+  }
+
   // Simulate charging station restart
   private handleRequestReset(
     chargingStation: ChargingStation,
@@ -338,23 +514,6 @@ export default class OCPP16IncomingRequestService extends OCPPIncomingRequestSer
     chargingStation: ChargingStation,
     commandPayload: ChangeConfigurationRequest
   ): ChangeConfigurationResponse {
-    // JSON request fields type sanity check
-    if (!Utils.isString(commandPayload.key)) {
-      logger.error(
-        `${chargingStation.logPrefix()} ${
-          OCPP16IncomingRequestCommand.CHANGE_CONFIGURATION
-        } request key field is not a string:`,
-        commandPayload
-      );
-    }
-    if (!Utils.isString(commandPayload.value)) {
-      logger.error(
-        `${chargingStation.logPrefix()} ${
-          OCPP16IncomingRequestCommand.CHANGE_CONFIGURATION
-        } request value field is not a string:`,
-        commandPayload
-      );
-    }
     const keyToChange = ChargingStationConfigurationUtils.getConfigurationKey(
       chargingStation,
       commandPayload.key,
@@ -603,6 +762,15 @@ export default class OCPP16IncomingRequestService extends OCPPIncomingRequestSer
     const transactionConnectorId = commandPayload.connectorId;
     const connectorStatus = chargingStation.getConnectorStatus(transactionConnectorId);
     if (transactionConnectorId) {
+      const remoteStartTransactionLogMsg =
+        chargingStation.logPrefix() +
+        ' Transaction remotely STARTED on ' +
+        chargingStation.stationInfo.chargingStationId +
+        '#' +
+        transactionConnectorId.toString() +
+        " for idTag '" +
+        commandPayload.idTag +
+        "'";
       await chargingStation.ocppRequestService.requestHandler<
         OCPP16StatusNotificationRequest,
         OCPP16StatusNotificationResponse
@@ -628,7 +796,7 @@ export default class OCPP16IncomingRequestService extends OCPPIncomingRequestSer
             connectorStatus.localAuthorizeIdTag = commandPayload.idTag;
             connectorStatus.idTagLocalAuthorized = true;
             authorized = true;
-          } else if (chargingStation.getMayAuthorizeAtRemoteStart()) {
+          } else if (chargingStation.getMustAuthorizeAtRemoteStart()) {
             connectorStatus.authorizeIdTag = commandPayload.idTag;
             const authorizeResponse: OCPP16AuthorizeResponse =
               await chargingStation.ocppRequestService.requestHandler<
@@ -666,15 +834,7 @@ export default class OCPP16IncomingRequestService extends OCPPIncomingRequestSer
                   })
                 ).idTagInfo.status === OCPP16AuthorizationStatus.ACCEPTED
               ) {
-                logger.debug(
-                  chargingStation.logPrefix() +
-                    ' Transaction remotely STARTED on ' +
-                    chargingStation.stationInfo.chargingStationId +
-                    '#' +
-                    transactionConnectorId.toString() +
-                    ' for idTag ' +
-                    commandPayload.idTag
-                );
+                logger.debug(remoteStartTransactionLogMsg);
                 return Constants.OCPP_RESPONSE_ACCEPTED;
               }
               return this.notifyRemoteStartTransactionRejected(
@@ -715,15 +875,7 @@ export default class OCPP16IncomingRequestService extends OCPPIncomingRequestSer
               })
             ).idTagInfo.status === OCPP16AuthorizationStatus.ACCEPTED
           ) {
-            logger.debug(
-              chargingStation.logPrefix() +
-                ' Transaction remotely STARTED on ' +
-                chargingStation.stationInfo.chargingStationId +
-                '#' +
-                transactionConnectorId.toString() +
-                ' for idTag ' +
-                commandPayload.idTag
-            );
+            logger.debug(remoteStartTransactionLogMsg);
             return Constants.OCPP_RESPONSE_ACCEPTED;
           }
           return this.notifyRemoteStartTransactionRejected(
@@ -773,12 +925,13 @@ export default class OCPP16IncomingRequestService extends OCPPIncomingRequestSer
       chargingStation.logPrefix() +
         ' Remote starting transaction REJECTED on connector Id ' +
         connectorId.toString() +
-        ', idTag ' +
+        ", idTag '" +
         idTag +
-        ', availability ' +
+        "', availability '" +
         chargingStation.getConnectorStatus(connectorId).availability +
-        ', status ' +
-        chargingStation.getConnectorStatus(connectorId).status
+        "', status '" +
+        chargingStation.getConnectorStatus(connectorId).status +
+        "'"
     );
     return Constants.OCPP_RESPONSE_REJECTED;
   }