More strict boolean checks
[e-mobility-charging-stations-simulator.git] / src / charging-station / ocpp / 1.6 / OCPP16ResponseService.ts
index 3af8a1fc5b8d3dcdb6f3f5bbdfa01b7664848667..4ba89706feb8a4a38a52edcad386f880122090e9 100644 (file)
@@ -1,11 +1,17 @@
 // Partial Copyright Jerome Benoit. 2021. All Rights Reserved.
 
+import fs from 'fs';
+import path from 'path';
+import { fileURLToPath } from 'url';
+
+import type { JSONSchemaType } from 'ajv';
+
 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 { OCPP16StandardParametersKey } from '../../../types/ocpp/1.6/Configuration';
-import {
+import type {
   OCPP16MeterValuesRequest,
   OCPP16MeterValuesResponse,
 } from '../../../types/ocpp/1.6/MeterValues';
@@ -15,7 +21,9 @@ import {
   OCPP16StatusNotificationRequest,
 } from '../../../types/ocpp/1.6/Requests';
 import {
+  DiagnosticsStatusNotificationResponse,
   OCPP16BootNotificationResponse,
+  OCPP16HeartbeatResponse,
   OCPP16RegistrationStatus,
   OCPP16StatusNotificationResponse,
 } from '../../../types/ocpp/1.6/Responses';
@@ -29,12 +37,12 @@ import {
   OCPP16StopTransactionResponse,
 } from '../../../types/ocpp/1.6/Transaction';
 import { ErrorType } from '../../../types/ocpp/ErrorType';
-import { ResponseHandler } from '../../../types/ocpp/Responses';
+import type { ResponseHandler } 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 OCPPResponseService from '../OCPPResponseService';
 import { OCPP16ServiceUtils } from './OCPP16ServiceUtils';
 
@@ -42,6 +50,7 @@ const moduleName = 'OCPP16ResponseService';
 
 export default class OCPP16ResponseService extends OCPPResponseService {
   private responseHandlers: Map<OCPP16RequestCommand, ResponseHandler>;
+  private jsonSchemas: Map<OCPP16RequestCommand, JSONSchemaType<JsonObject>>;
 
   public constructor() {
     if (new.target?.name === moduleName) {
@@ -50,13 +59,113 @@ export default class OCPP16ResponseService extends OCPPResponseService {
     super();
     this.responseHandlers = new Map<OCPP16RequestCommand, ResponseHandler>([
       [OCPP16RequestCommand.BOOT_NOTIFICATION, this.handleResponseBootNotification.bind(this)],
-      [OCPP16RequestCommand.HEARTBEAT, this.handleResponseHeartbeat.bind(this)],
+      [OCPP16RequestCommand.HEARTBEAT, this.emptyResponseHandler.bind(this)],
       [OCPP16RequestCommand.AUTHORIZE, this.handleResponseAuthorize.bind(this)],
       [OCPP16RequestCommand.START_TRANSACTION, this.handleResponseStartTransaction.bind(this)],
       [OCPP16RequestCommand.STOP_TRANSACTION, this.handleResponseStopTransaction.bind(this)],
-      [OCPP16RequestCommand.STATUS_NOTIFICATION, this.handleResponseStatusNotification.bind(this)],
-      [OCPP16RequestCommand.METER_VALUES, this.handleResponseMeterValues.bind(this)],
+      [OCPP16RequestCommand.STATUS_NOTIFICATION, this.emptyResponseHandler.bind(this)],
+      [OCPP16RequestCommand.METER_VALUES, this.emptyResponseHandler.bind(this)],
+      [OCPP16RequestCommand.DIAGNOSTICS_STATUS_NOTIFICATION, this.emptyResponseHandler.bind(this)],
     ]);
+    this.jsonSchemas = new Map<OCPP16RequestCommand, JSONSchemaType<JsonObject>>([
+      [
+        OCPP16RequestCommand.BOOT_NOTIFICATION,
+        JSON.parse(
+          fs.readFileSync(
+            path.resolve(
+              path.dirname(fileURLToPath(import.meta.url)),
+              '../../../assets/json-schemas/ocpp/1.6/BootNotificationResponse.json'
+            ),
+            'utf8'
+          )
+        ) as JSONSchemaType<OCPP16BootNotificationResponse>,
+      ],
+      [
+        OCPP16RequestCommand.HEARTBEAT,
+        JSON.parse(
+          fs.readFileSync(
+            path.resolve(
+              path.dirname(fileURLToPath(import.meta.url)),
+              '../../../assets/json-schemas/ocpp/1.6/HeartbeatResponse.json'
+            ),
+            'utf8'
+          )
+        ) as JSONSchemaType<OCPP16HeartbeatResponse>,
+      ],
+      [
+        OCPP16RequestCommand.AUTHORIZE,
+        JSON.parse(
+          fs.readFileSync(
+            path.resolve(
+              path.dirname(fileURLToPath(import.meta.url)),
+              '../../../assets/json-schemas/ocpp/1.6/AuthorizeResponse.json'
+            ),
+            'utf8'
+          )
+        ) as JSONSchemaType<OCPP16AuthorizeResponse>,
+      ],
+      [
+        OCPP16RequestCommand.START_TRANSACTION,
+        JSON.parse(
+          fs.readFileSync(
+            path.resolve(
+              path.dirname(fileURLToPath(import.meta.url)),
+              '../../../assets/json-schemas/ocpp/1.6/StartTransactionResponse.json'
+            ),
+            'utf8'
+          )
+        ) as JSONSchemaType<OCPP16StartTransactionResponse>,
+      ],
+      [
+        OCPP16RequestCommand.STOP_TRANSACTION,
+        JSON.parse(
+          fs.readFileSync(
+            path.resolve(
+              path.dirname(fileURLToPath(import.meta.url)),
+              '../../../assets/json-schemas/ocpp/1.6/StopTransactionResponse.json'
+            ),
+            'utf8'
+          )
+        ) as JSONSchemaType<OCPP16StopTransactionResponse>,
+      ],
+      [
+        OCPP16RequestCommand.STATUS_NOTIFICATION,
+        JSON.parse(
+          fs.readFileSync(
+            path.resolve(
+              path.dirname(fileURLToPath(import.meta.url)),
+              '../../../assets/json-schemas/ocpp/1.6/StatusNotificationResponse.json'
+            ),
+            'utf8'
+          )
+        ) as JSONSchemaType<OCPP16StatusNotificationResponse>,
+      ],
+      [
+        OCPP16RequestCommand.METER_VALUES,
+        JSON.parse(
+          fs.readFileSync(
+            path.resolve(
+              path.dirname(fileURLToPath(import.meta.url)),
+              '../../../assets/json-schemas/ocpp/1.6/MeterValuesResponse.json'
+            ),
+            'utf8'
+          )
+        ) as JSONSchemaType<OCPP16MeterValuesResponse>,
+      ],
+      [
+        OCPP16RequestCommand.DIAGNOSTICS_STATUS_NOTIFICATION,
+        JSON.parse(
+          fs.readFileSync(
+            path.resolve(
+              path.dirname(fileURLToPath(import.meta.url)),
+              '../../../assets/json-schemas/ocpp/1.6/DiagnosticsStatusNotificationResponse.json'
+            ),
+            'utf8'
+          )
+        ) as JSONSchemaType<DiagnosticsStatusNotificationResponse>,
+      ],
+    ]);
+    this.validatePayload.bind(this);
   }
 
   public async responseHandler(
@@ -65,22 +174,29 @@ export default class OCPP16ResponseService extends OCPPResponseService {
     payload: JsonType,
     requestPayload: JsonType
   ): Promise<void> {
-    if (chargingStation.isRegistered() || commandName === OCPP16RequestCommand.BOOT_NOTIFICATION) {
+    if (
+      chargingStation.isRegistered() === true ||
+      commandName === OCPP16RequestCommand.BOOT_NOTIFICATION
+    ) {
       if (
-        this.responseHandlers.has(commandName) &&
-        ChargingStationUtils.isCommandSupported(commandName, chargingStation)
+        this.responseHandlers.has(commandName) === true &&
+        OCPP16ServiceUtils.isRequestCommandSupported(chargingStation, commandName) === true
       ) {
         try {
+          this.validatePayload(chargingStation, commandName, payload);
           await this.responseHandlers.get(commandName)(chargingStation, payload, requestPayload);
         } catch (error) {
-          logger.error(chargingStation.logPrefix() + ' Handle request response error: %j', error);
+          logger.error(
+            `${chargingStation.logPrefix()} ${moduleName}.responseHandler: Handle response error:`,
+            error
+          );
           throw error;
         }
       } else {
         // Throw exception
         throw new OCPPError(
           ErrorType.NOT_IMPLEMENTED,
-          `${commandName} is not implemented to handle request response payload ${JSON.stringify(
+          `${commandName} is not implemented to handle response PDU ${JSON.stringify(
             payload,
             null,
             2
@@ -92,7 +208,7 @@ export default class OCPP16ResponseService extends OCPPResponseService {
     } else {
       throw new OCPPError(
         ErrorType.SECURITY_ERROR,
-        `${commandName} cannot be issued to handle request response payload ${JSON.stringify(
+        `${commandName} cannot be issued to handle response PDU ${JSON.stringify(
           payload,
           null,
           2
@@ -103,6 +219,25 @@ export default class OCPP16ResponseService extends OCPPResponseService {
     }
   }
 
+  private validatePayload(
+    chargingStation: ChargingStation,
+    commandName: OCPP16RequestCommand,
+    payload: JsonType
+  ): boolean {
+    if (this.jsonSchemas.has(commandName)) {
+      return this.validateResponsePayload(
+        chargingStation,
+        commandName,
+        this.jsonSchemas.get(commandName),
+        payload
+      );
+    }
+    logger.warn(
+      `${chargingStation.logPrefix()} ${moduleName}.validatePayload: No JSON schema found for command ${commandName} PDU validation`
+    );
+    return false;
+  }
+
   private handleResponseBootNotification(
     chargingStation: ChargingStation,
     payload: OCPP16BootNotificationResponse
@@ -142,9 +277,6 @@ export default class OCPP16ResponseService extends OCPPResponseService {
     }
   }
 
-  // eslint-disable-next-line @typescript-eslint/no-empty-function
-  private handleResponseHeartbeat(): void {}
-
   private handleResponseAuthorize(
     chargingStation: ChargingStation,
     payload: OCPP16AuthorizeResponse,
@@ -160,20 +292,24 @@ export default class OCPP16ResponseService extends OCPPResponseService {
         break;
       }
     }
+    const isAuthorizeConnectorIdDefined = authorizeConnectorId !== undefined;
     if (payload.idTagInfo.status === OCPP16AuthorizationStatus.ACCEPTED) {
-      chargingStation.getConnectorStatus(authorizeConnectorId).idTagAuthorized = true;
+      isAuthorizeConnectorIdDefined &&
+        (chargingStation.getConnectorStatus(authorizeConnectorId).idTagAuthorized = true);
       logger.debug(
-        `${chargingStation.logPrefix()} IdTag ${
-          requestPayload.idTag
-        } authorized on connector ${authorizeConnectorId}`
+        `${chargingStation.logPrefix()} IdTag '${requestPayload.idTag}' accepted${
+          isAuthorizeConnectorIdDefined ? ` on connector ${authorizeConnectorId}` : ''
+        }`
       );
     } else {
-      chargingStation.getConnectorStatus(authorizeConnectorId).idTagAuthorized = false;
-      delete chargingStation.getConnectorStatus(authorizeConnectorId).authorizeIdTag;
+      if (isAuthorizeConnectorIdDefined) {
+        chargingStation.getConnectorStatus(authorizeConnectorId).idTagAuthorized = false;
+        delete chargingStation.getConnectorStatus(authorizeConnectorId).authorizeIdTag;
+      }
       logger.debug(
-        `${chargingStation.logPrefix()} IdTag ${requestPayload.idTag} refused with status '${
+        `${chargingStation.logPrefix()} IdTag '${requestPayload.idTag}' rejected with status '${
           payload.idTagInfo.status
-        }' on connector ${authorizeConnectorId}`
+        }'${isAuthorizeConnectorIdDefined ? ` on connector ${authorizeConnectorId}` : ''}`
       );
     }
   }
@@ -201,11 +337,11 @@ export default class OCPP16ResponseService extends OCPPResponseService {
       return;
     }
     if (
-      chargingStation.getConnectorStatus(connectorId).transactionRemoteStarted &&
+      chargingStation.getConnectorStatus(connectorId).transactionRemoteStarted === true &&
       chargingStation.getAuthorizeRemoteTxRequests() &&
       chargingStation.getLocalAuthListEnabled() &&
       chargingStation.hasAuthorizedTags() &&
-      !chargingStation.getConnectorStatus(connectorId).idTagLocalAuthorized
+      chargingStation.getConnectorStatus(connectorId).idTagLocalAuthorized === false
     ) {
       logger.error(
         chargingStation.logPrefix() +
@@ -218,11 +354,11 @@ export default class OCPP16ResponseService extends OCPPResponseService {
       return;
     }
     if (
-      chargingStation.getConnectorStatus(connectorId).transactionRemoteStarted &&
+      chargingStation.getConnectorStatus(connectorId).transactionRemoteStarted === true &&
       chargingStation.getAuthorizeRemoteTxRequests() &&
-      chargingStation.getMayAuthorizeAtRemoteStart() &&
-      !chargingStation.getConnectorStatus(connectorId).idTagLocalAuthorized &&
-      !chargingStation.getConnectorStatus(connectorId).idTagAuthorized
+      chargingStation.getMustAuthorizeAtRemoteStart() &&
+      chargingStation.getConnectorStatus(connectorId).idTagLocalAuthorized === false &&
+      chargingStation.getConnectorStatus(connectorId).idTagAuthorized === false
     ) {
       logger.error(
         chargingStation.logPrefix() +
@@ -266,7 +402,7 @@ export default class OCPP16ResponseService extends OCPPResponseService {
       await this.resetConnectorOnStartTransactionError(chargingStation, connectorId);
       return;
     }
-    if (chargingStation.getConnectorStatus(connectorId)?.transactionStarted) {
+    if (chargingStation.getConnectorStatus(connectorId)?.transactionStarted === true) {
       logger.debug(
         chargingStation.logPrefix() +
           ' Trying to start a transaction on an already used connector ' +
@@ -288,14 +424,14 @@ export default class OCPP16ResponseService extends OCPPResponseService {
       );
       return;
     }
-    if (!Number.isInteger(payload.transactionId)) {
-      logger.warn(
-        `${chargingStation.logPrefix()} Trying to start a transaction on connector ${connectorId.toString()} with a non integer transaction Id ${
-          payload.transactionId
-        }, converting to integer`
-      );
-      payload.transactionId = Utils.convertToInt(payload.transactionId);
-    }
+    // if (!Number.isInteger(payload.transactionId)) {
+    //   logger.warn(
+    //     `${chargingStation.logPrefix()} Trying to start a transaction on connector ${connectorId.toString()} with a non integer transaction Id ${
+    //       payload.transactionId
+    //     }, converting to integer`
+    //   );
+    //   payload.transactionId = Utils.convertToInt(payload.transactionId);
+    // }
 
     if (payload.idTagInfo?.status === OCPP16AuthorizationStatus.ACCEPTED) {
       chargingStation.getConnectorStatus(connectorId).transactionStarted = true;
@@ -336,8 +472,9 @@ export default class OCPP16ResponseService extends OCPPResponseService {
           chargingStation.stationInfo.chargingStationId +
           '#' +
           connectorId.toString() +
-          ' for idTag ' +
-          requestPayload.idTag
+          " for idTag '" +
+          requestPayload.idTag +
+          "'"
       );
       if (chargingStation.stationInfo.powerSharedByConnectors) {
         chargingStation.powerDivider++;
@@ -351,7 +488,7 @@ export default class OCPP16ResponseService extends OCPPResponseService {
         connectorId,
         configuredMeterValueSampleInterval
           ? Utils.convertToInt(configuredMeterValueSampleInterval.value) * 1000
-          : 60000
+          : Constants.DEFAULT_METER_VALUES_INTERVAL
       );
     } else {
       logger.warn(
@@ -360,8 +497,9 @@ export default class OCPP16ResponseService extends OCPPResponseService {
           payload.transactionId.toString() +
           " REJECTED with status '" +
           payload?.idTagInfo?.status +
-          "', idTag " +
-          requestPayload.idTag
+          "', idTag '" +
+          requestPayload.idTag +
+          "'"
       );
       await this.resetConnectorOnStartTransactionError(chargingStation, connectorId);
     }
@@ -422,8 +560,8 @@ export default class OCPP16ResponseService extends OCPPResponseService {
           ],
         }));
       if (
-        !chargingStation.isChargingStationAvailable() ||
-        !chargingStation.isConnectorAvailable(transactionConnectorId)
+        chargingStation.isChargingStationAvailable() === false ||
+        chargingStation.isConnectorAvailable(transactionConnectorId) === false
       ) {
         await chargingStation.ocppRequestService.requestHandler<
           OCPP16StatusNotificationRequest,
@@ -450,6 +588,7 @@ export default class OCPP16ResponseService extends OCPPResponseService {
       if (chargingStation.stationInfo.powerSharedByConnectors) {
         chargingStation.powerDivider--;
       }
+      chargingStation.resetConnectorStatus(transactionConnectorId);
       logger.info(
         chargingStation.logPrefix() +
           ' Transaction ' +
@@ -459,7 +598,6 @@ export default class OCPP16ResponseService extends OCPPResponseService {
           '#' +
           transactionConnectorId.toString()
       );
-      chargingStation.resetConnectorStatus(transactionConnectorId);
     } else {
       logger.warn(
         chargingStation.logPrefix() +
@@ -471,10 +609,4 @@ export default class OCPP16ResponseService extends OCPPResponseService {
       );
     }
   }
-
-  // eslint-disable-next-line @typescript-eslint/no-empty-function
-  private handleResponseStatusNotification(): void {}
-
-  // eslint-disable-next-line @typescript-eslint/no-empty-function
-  private handleResponseMeterValues(): void {}
 }