UI Protocol: add Authorize command support
[e-mobility-charging-stations-simulator.git] / src / charging-station / ocpp / 1.6 / OCPP16ResponseService.ts
index 3372e1036c1ce464e7e1700fb87cca114177170a..89f8ffd076a43f9e646f8c93549940cdd081cbe1 100644 (file)
@@ -4,14 +4,14 @@ import fs from 'fs';
 import path from 'path';
 import { fileURLToPath } from 'url';
 
-import { JSONSchemaType } from 'ajv';
+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';
@@ -21,6 +21,7 @@ import {
   OCPP16StatusNotificationRequest,
 } from '../../../types/ocpp/1.6/Requests';
 import {
+  DiagnosticsStatusNotificationResponse,
   OCPP16BootNotificationResponse,
   OCPP16HeartbeatResponse,
   OCPP16RegistrationStatus,
@@ -36,7 +37,7 @@ 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 logger from '../../../utils/Logger';
 import Utils from '../../../utils/Utils';
 import type ChargingStation from '../../ChargingStation';
@@ -49,13 +50,7 @@ const moduleName = 'OCPP16ResponseService';
 
 export default class OCPP16ResponseService extends OCPPResponseService {
   private responseHandlers: Map<OCPP16RequestCommand, ResponseHandler>;
-  private bootNotificationResponseJsonSchema: JSONSchemaType<OCPP16BootNotificationResponse>;
-  private heartbeatResponseJsonSchema: JSONSchemaType<OCPP16HeartbeatResponse>;
-  private authorizeResponseJsonSchema: JSONSchemaType<OCPP16AuthorizeResponse>;
-  private startTransactionResponseJsonSchema: JSONSchemaType<OCPP16StartTransactionResponse>;
-  private stopTransactionResponseJsonSchema: JSONSchemaType<OCPP16StopTransactionResponse>;
-  private statusNotificationResponseJsonSchema: JSONSchemaType<OCPP16StatusNotificationResponse>;
-  private meterValuesResponseJsonSchema: JSONSchemaType<OCPP16MeterValuesResponse>;
+  private jsonSchemas: Map<OCPP16RequestCommand, JSONSchemaType<JsonObject>>;
 
   public constructor() {
     if (new.target?.name === moduleName) {
@@ -64,76 +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.bootNotificationResponseJsonSchema = JSON.parse(
-      fs.readFileSync(
-        path.resolve(
-          path.dirname(fileURLToPath(import.meta.url)),
-          '../../../assets/json-schemas/ocpp/1.6/BootNotificationResponse.json'
-        ),
-        'utf8'
-      )
-    ) as JSONSchemaType<OCPP16BootNotificationResponse>;
-    this.heartbeatResponseJsonSchema = JSON.parse(
-      fs.readFileSync(
-        path.resolve(
-          path.dirname(fileURLToPath(import.meta.url)),
-          '../../../assets/json-schemas/ocpp/1.6/HeartbeatResponse.json'
-        ),
-        'utf8'
-      )
-    ) as JSONSchemaType<OCPP16HeartbeatResponse>;
-    this.authorizeResponseJsonSchema = JSON.parse(
-      fs.readFileSync(
-        path.resolve(
-          path.dirname(fileURLToPath(import.meta.url)),
-          '../../../assets/json-schemas/ocpp/1.6/AuthorizeResponse.json'
-        ),
-        'utf8'
-      )
-    ) as JSONSchemaType<OCPP16AuthorizeResponse>;
-    this.startTransactionResponseJsonSchema = JSON.parse(
-      fs.readFileSync(
-        path.resolve(
-          path.dirname(fileURLToPath(import.meta.url)),
-          '../../../assets/json-schemas/ocpp/1.6/StartTransactionResponse.json'
-        ),
-        'utf8'
-      )
-    ) as JSONSchemaType<OCPP16StartTransactionResponse>;
-    this.stopTransactionResponseJsonSchema = JSON.parse(
-      fs.readFileSync(
-        path.resolve(
-          path.dirname(fileURLToPath(import.meta.url)),
-          '../../../assets/json-schemas/ocpp/1.6/StopTransactionResponse.json'
-        ),
-        'utf8'
-      )
-    ) as JSONSchemaType<OCPP16StopTransactionResponse>;
-    this.statusNotificationResponseJsonSchema = JSON.parse(
-      fs.readFileSync(
-        path.resolve(
-          path.dirname(fileURLToPath(import.meta.url)),
-          '../../../assets/json-schemas/ocpp/1.6/StatusNotificationResponse.json'
-        ),
-        'utf8'
-      )
-    ) as JSONSchemaType<OCPP16StatusNotificationResponse>;
-    this.meterValuesResponseJsonSchema = JSON.parse(
-      fs.readFileSync(
-        path.resolve(
-          path.dirname(fileURLToPath(import.meta.url)),
-          '../../../assets/json-schemas/ocpp/1.6/MeterValuesResponse.json'
-        ),
-        'utf8'
-      )
-    ) as JSONSchemaType<OCPP16MeterValuesResponse>;
+    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(
@@ -148,16 +180,20 @@ export default class OCPP16ResponseService extends OCPPResponseService {
         ChargingStationUtils.isRequestCommandSupported(commandName, chargingStation)
       ) {
         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 PDU ${JSON.stringify(
+          `${commandName} is not implemented to handle response PDU ${JSON.stringify(
             payload,
             null,
             2
@@ -169,7 +205,7 @@ export default class OCPP16ResponseService extends OCPPResponseService {
     } else {
       throw new OCPPError(
         ErrorType.SECURITY_ERROR,
-        `${commandName} cannot be issued to handle request response PDU ${JSON.stringify(
+        `${commandName} cannot be issued to handle response PDU ${JSON.stringify(
           payload,
           null,
           2
@@ -180,16 +216,29 @@ 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
   ): void {
-    this.validateResponsePayload(
-      chargingStation,
-      OCPP16RequestCommand.BOOT_NOTIFICATION,
-      this.bootNotificationResponseJsonSchema,
-      payload
-    );
     if (payload.status === OCPP16RegistrationStatus.ACCEPTED) {
       ChargingStationConfigurationUtils.addConfigurationKey(
         chargingStation,
@@ -225,29 +274,11 @@ export default class OCPP16ResponseService extends OCPPResponseService {
     }
   }
 
-  private handleResponseHeartbeat(
-    chargingStation: ChargingStation,
-    payload: OCPP16HeartbeatResponse
-  ): void {
-    this.validateResponsePayload(
-      chargingStation,
-      OCPP16RequestCommand.HEARTBEAT,
-      this.heartbeatResponseJsonSchema,
-      payload
-    );
-  }
-
   private handleResponseAuthorize(
     chargingStation: ChargingStation,
     payload: OCPP16AuthorizeResponse,
     requestPayload: OCPP16AuthorizeRequest
   ): void {
-    this.validateResponsePayload(
-      chargingStation,
-      OCPP16RequestCommand.AUTHORIZE,
-      this.authorizeResponseJsonSchema,
-      payload
-    );
     let authorizeConnectorId: number;
     for (const connectorId of chargingStation.connectors.keys()) {
       if (
@@ -258,20 +289,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}` : ''}`
       );
     }
   }
@@ -281,12 +316,6 @@ export default class OCPP16ResponseService extends OCPPResponseService {
     payload: OCPP16StartTransactionResponse,
     requestPayload: OCPP16StartTransactionRequest
   ): Promise<void> {
-    this.validateResponsePayload(
-      chargingStation,
-      OCPP16RequestCommand.START_TRANSACTION,
-      this.startTransactionResponseJsonSchema,
-      payload
-    );
     const connectorId = requestPayload.connectorId;
 
     let transactionConnectorId: number;
@@ -305,11 +334,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() +
@@ -322,11 +351,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() +
@@ -370,7 +399,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 ' +
@@ -392,14 +421,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;
@@ -440,8 +469,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++;
@@ -464,8 +494,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);
     }
@@ -496,12 +527,6 @@ export default class OCPP16ResponseService extends OCPPResponseService {
     payload: OCPP16StopTransactionResponse,
     requestPayload: OCPP16StopTransactionRequest
   ): Promise<void> {
-    this.validateResponsePayload(
-      chargingStation,
-      OCPP16RequestCommand.STOP_TRANSACTION,
-      this.stopTransactionResponseJsonSchema,
-      payload
-    );
     const transactionConnectorId = chargingStation.getConnectorIdByTransactionId(
       requestPayload.transactionId
     );
@@ -560,6 +585,7 @@ export default class OCPP16ResponseService extends OCPPResponseService {
       if (chargingStation.stationInfo.powerSharedByConnectors) {
         chargingStation.powerDivider--;
       }
+      chargingStation.resetConnectorStatus(transactionConnectorId);
       logger.info(
         chargingStation.logPrefix() +
           ' Transaction ' +
@@ -569,7 +595,6 @@ export default class OCPP16ResponseService extends OCPPResponseService {
           '#' +
           transactionConnectorId.toString()
       );
-      chargingStation.resetConnectorStatus(transactionConnectorId);
     } else {
       logger.warn(
         chargingStation.logPrefix() +
@@ -581,28 +606,4 @@ export default class OCPP16ResponseService extends OCPPResponseService {
       );
     }
   }
-
-  private handleResponseStatusNotification(
-    chargingStation: ChargingStation,
-    payload: OCPP16StatusNotificationResponse
-  ): void {
-    this.validateResponsePayload(
-      chargingStation,
-      OCPP16RequestCommand.STATUS_NOTIFICATION,
-      this.statusNotificationResponseJsonSchema,
-      payload
-    );
-  }
-
-  private handleResponseMeterValues(
-    chargingStation: ChargingStation,
-    payload: OCPP16MeterValuesResponse
-  ): void {
-    this.validateResponsePayload(
-      chargingStation,
-      OCPP16RequestCommand.METER_VALUES,
-      this.meterValuesResponseJsonSchema,
-      payload
-    );
-  }
 }