Add occpStrictCompliance template tunable
authorJérôme Benoit <jerome.benoit@sap.com>
Wed, 9 Feb 2022 17:00:44 +0000 (18:00 +0100)
committerJérôme Benoit <jerome.benoit@sap.com>
Wed, 9 Feb 2022 17:00:44 +0000 (18:00 +0100)
Signed-off-by: Jérôme Benoit <jerome.benoit@sap.com>
README.md
src/charging-station/ChargingStation.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/OCPPRequestService.ts
src/types/ChargingStationTemplate.ts
src/utils/Constants.ts

index 9f1156e75fe46147311288736462254b850599e6..29bf46dd9c96c11ab77fced223a91f76ddad70b9 100644 (file)
--- a/README.md
+++ b/README.md
@@ -84,13 +84,14 @@ stationTemplateUrls | | {}[] | { file: string; numberOfStations: number; }[] | a
 
 Key | Value(s) | Default Value | Value type | Description 
 --- | -------| --------------| ---------- | ------------
-supervisionUrls | | '' | string \| string[] | string or array of connection URIs to OCPP-J servers. It has priority over the global configuration parameter
+supervisionUrls | | '' | string\|string[] | string or array of connection URIs to OCPP-J servers. It has priority over the global configuration parameter
 supervisionUser | | '' | string | basic HTTP authentication user to OCPP-J server
 supervisionPassword | | '' | string | basic HTTP authentication password to OCPP-J server
 supervisionUrlOcppConfiguration | true/false | false | boolean | Allow supervision URL configuration via a vendor OCPP parameter key
 supervisionUrlOcppKey | | 'ConnectionUrl' | string | The vendor string that will be used as a vendor OCPP parameter key to set the supervision URL
 ocppVersion | 1.6 | 1.6 | string | OCPP version 
 ocppProtocol | json | json | string | OCPP protocol
+ocppStrictCompliance | true/false | true | boolean | Strict adherence to the OCPP version and protocol specifications
 wsOptions | | {} | ClientOptions & ClientRequestArgs | [ws](https://github.com/websockets/ws) and node.js [http](https://nodejs.org/api/http.html) clients options intersection
 authorizationFile | | '' | string | RFID tags list file relative to src/assets path
 baseName | | '' | string | base name to build charging stations name
@@ -117,7 +118,7 @@ registrationMaxRetries | | -1 (unlimited) | integer | charging stations boot not
 enableStatistics | true/false | true | boolean | enable charging stations statistics
 mayAuthorizeAtRemoteStart | true/false | true | boolean | always send authorize at remote start transaction when AuthorizeRemoteTxRequests is enabled
 beginEndMeterValues | true/false | false | boolean | enable Transaction.{Begin,End} MeterValues
-outOfOrderEndMeterValues | true/false | false | boolean | send Transaction.End MeterValues out of order
+outOfOrderEndMeterValues | true/false | false | boolean | send Transaction.End MeterValues out of order. Need to relax OCPP specifications strict compliance ('ocppStrictCompliance' parameter)
 meteringPerTransaction | true/false | true | boolean | enable metering history on a per transaction basis
 transactionDataMeterValues | true/false | false | boolean | enable transaction data MeterValues at stop transaction
 mainVoltageMeterValues | true/false | true | boolean | include charging station main voltage MeterValues on three phased charging stations
index 488333379b3f847ef6749efc70fcbb10e46577ce..8d2efa755324f07e7a51064eb5651eb9107ae70b 100644 (file)
@@ -123,6 +123,10 @@ export default class ChargingStation {
     return this?.wsConnection?.readyState === OPEN;
   }
 
+  public getRegistrationStatus(): RegistrationStatus {
+    return this?.bootNotificationResponse?.status;
+  }
+
   public isInUnknownState(): boolean {
     return Utils.isNullOrUndefined(this?.bootNotificationResponse?.status);
   }
@@ -163,6 +167,10 @@ export default class ChargingStation {
     return this.stationInfo.currentOutType ?? CurrentType.AC;
   }
 
+  public getOcppStrictCompliance(): boolean {
+    return this.stationInfo.ocppStrictCompliance ?? false;
+  }
+
   public getVoltageOut(): number | undefined {
     const errMsg = `${this.logPrefix()} Unknown ${this.getCurrentOutType()} currentOutType in template file ${this.stationTemplateFile}, cannot define default voltage out`;
     let defaultVoltageOut: number;
@@ -645,19 +653,19 @@ export default class ChargingStation {
 
   private async onOpen(): Promise<void> {
     logger.info(`${this.logPrefix()} Connected to OCPP server through ${this.wsConnectionUrl.toString()}`);
-    if (!this.isRegistered()) {
+    if (!this.isInAcceptedState()) {
       // Send BootNotification
       let registrationRetryCount = 0;
       do {
         this.bootNotificationResponse = await this.ocppRequestService.sendBootNotification(this.bootNotificationRequest.chargePointModel,
           this.bootNotificationRequest.chargePointVendor, this.bootNotificationRequest.chargeBoxSerialNumber, this.bootNotificationRequest.firmwareVersion);
-        if (!this.isRegistered()) {
-          registrationRetryCount++;
+        if (!this.isInAcceptedState()) {
+          this.getRegistrationMaxRetries() !== -1 && registrationRetryCount++;
           await Utils.sleep(this.bootNotificationResponse?.interval ? this.bootNotificationResponse.interval * 1000 : Constants.OCPP_DEFAULT_BOOT_NOTIFICATION_INTERVAL);
         }
-      } while (!this.isRegistered() && (registrationRetryCount <= this.getRegistrationMaxRetries() || this.getRegistrationMaxRetries() === -1));
+      } while (!this.isInAcceptedState() && (registrationRetryCount <= this.getRegistrationMaxRetries() || this.getRegistrationMaxRetries() === -1));
     }
-    if (this.isRegistered() && this.stationInfo.autoRegister) {
+    if (this.isInAcceptedState() && this.stationInfo.autoRegister) {
       await this.ocppRequestService.sendBootNotification(this.bootNotificationRequest.chargePointModel,
         this.bootNotificationRequest.chargePointVendor, this.bootNotificationRequest.chargeBoxSerialNumber, this.bootNotificationRequest.firmwareVersion);
     }
@@ -667,16 +675,6 @@ export default class ChargingStation {
       if (this.wsConnectionRestarted && this.isWebSocketConnectionOpened()) {
         this.flushMessageBuffer();
       }
-    } else if (this.isInPendingState()) {
-      // The central server shall issue a TriggerMessage to the charging station for the boot notification at the end of its configuration process
-      while (!this.isInAcceptedState()) {
-        await Utils.sleep(Constants.CHARGING_STATION_DEFAULT_START_SEQUENCE_DELAY);
-      }
-      await this.startMessageSequence();
-      this.stopped && (this.stopped = false);
-      if (this.wsConnectionRestarted && this.isWebSocketConnectionOpened()) {
-        this.flushMessageBuffer();
-      }
     } else {
       logger.error(`${this.logPrefix()} Registration failure: max retries reached (${this.getRegistrationMaxRetries()}) or retry disabled (${this.getRegistrationMaxRetries()})`);
     }
index 72e4d316d69f7f08d2bbe4489cd264d433fb10cc..c9f9042fb82fdd5ad5e5a707463eb8a4b5ccab49 100644 (file)
@@ -48,12 +48,11 @@ export default class OCPP16IncomingRequestService extends OCPPIncomingRequestSer
 
   public async handleRequest(messageId: string, commandName: OCPP16IncomingRequestCommand, commandPayload: JsonType): Promise<void> {
     let result: JsonType;
-    if (this.chargingStation.isInPendingState()
-      && (commandName === OCPP16IncomingRequestCommand.REMOTE_START_TRANSACTION || commandName === OCPP16IncomingRequestCommand.REMOTE_STOP_TRANSACTION)) {
-      throw new OCPPError(ErrorType.SECURITY_ERROR, `${commandName} cannot be issued to handle request payload ${JSON.stringify(commandPayload, null, 2)} while charging station is in pending state`, commandName);
+    if (this.chargingStation.getOcppStrictCompliance() && (this.chargingStation.isInPendingState()
+      && (commandName === OCPP16IncomingRequestCommand.REMOTE_START_TRANSACTION || commandName === OCPP16IncomingRequestCommand.REMOTE_STOP_TRANSACTION))) {
+      throw new OCPPError(ErrorType.SECURITY_ERROR, `${commandName} cannot be issued to handle request payload ${JSON.stringify(commandPayload, null, 2)} while the charging station is in pending state on the central server`, commandName);
     }
-    // FIXME: Add template tunable for accepting incoming configuration requests while in unknown state
-    if (this.chargingStation.isRegistered() || (this.chargingStation.isInUnknownState() && (commandName === OCPP16IncomingRequestCommand.GET_CONFIGURATION || commandName === OCPP16IncomingRequestCommand.CHANGE_CONFIGURATION || commandName === OCPP16IncomingRequestCommand.CHANGE_AVAILABILITY || commandName === OCPP16IncomingRequestCommand.TRIGGER_MESSAGE))) {
+    if (this.chargingStation.isRegistered() || (!this.chargingStation.getOcppStrictCompliance() && this.chargingStation.isInUnknownState())) {
       if (this.incomingRequestHandlers.has(commandName)) {
         try {
           // Call the method to build the result
@@ -68,7 +67,7 @@ export default class OCPP16IncomingRequestService extends OCPPIncomingRequestSer
         throw new OCPPError(ErrorType.NOT_IMPLEMENTED, `${commandName} is not implemented to handle request payload ${JSON.stringify(commandPayload, null, 2)}`, commandName);
       }
     } else {
-      throw new OCPPError(ErrorType.SECURITY_ERROR, `The charging station is not registered on the central server. ${commandName} cannot be issued to handle request payload ${JSON.stringify(commandPayload, null, 2)}`, commandName);
+      throw new OCPPError(ErrorType.SECURITY_ERROR, `${commandName} cannot be issued to handle request payload ${JSON.stringify(commandPayload, null, 2)} while the charging station is not registered on the central server.`, commandName);
     }
     // Send the built result
     await this.chargingStation.ocppRequestService.sendResult(messageId, result, commandName);
index 1fc89fa8c2d15b1bd0df8e3bed919d3063b6e2c3..3580cc52e18701ff05f8a6939b6714bb5be9113a 100644 (file)
@@ -107,7 +107,7 @@ export default class OCPP16RequestService extends OCPPRequestService {
       }
       const transactionEndMeterValue = OCPP16ServiceUtils.buildTransactionEndMeterValue(this.chargingStation, connectorId, meterStop);
       // FIXME: should be a callback, each OCPP commands implementation must do only one job
-      (this.chargingStation.getBeginEndMeterValues() && !this.chargingStation.getOutOfOrderEndMeterValues())
+      (this.chargingStation.getBeginEndMeterValues() && this.chargingStation.getOcppStrictCompliance() && !this.chargingStation.getOutOfOrderEndMeterValues())
         && await this.sendTransactionEndMeterValues(connectorId, transactionId, transactionEndMeterValue);
       const payload: StopTransactionRequest = {
         transactionId,
index 05e2ed9a4f7e0c4043a2d31d6caf616f7d73953f..23751c33d4365477ec0d1bb0bdf92e8bbf0e5de5 100644 (file)
@@ -4,6 +4,7 @@ import { AuthorizeRequest, OCPP16AuthorizationStatus, OCPP16AuthorizeResponse, O
 import { HeartbeatRequest, OCPP16RequestCommand, StatusNotificationRequest } from '../../../types/ocpp/1.6/Requests';
 import { HeartbeatResponse, OCPP16BootNotificationResponse, OCPP16RegistrationStatus, StatusNotificationResponse } from '../../../types/ocpp/1.6/Responses';
 import { MeterValuesRequest, MeterValuesResponse } from '../../../types/ocpp/1.6/MeterValues';
+import { RegistrationStatus, ResponseHandler } from '../../../types/ocpp/Responses';
 
 import ChargingStation from '../../ChargingStation';
 import { ErrorType } from '../../../types/ocpp/ErrorType';
@@ -13,7 +14,6 @@ import { OCPP16ServiceUtils } from './OCPP16ServiceUtils';
 import { OCPP16StandardParametersKey } from '../../../types/ocpp/1.6/Configuration';
 import OCPPError from '../../../exception/OCPPError';
 import OCPPResponseService from '../OCPPResponseService';
-import { ResponseHandler } from '../../../types/ocpp/Responses';
 import Utils from '../../../utils/Utils';
 import logger from '../../../utils/Logger';
 
@@ -47,7 +47,7 @@ export default class OCPP16ResponseService extends OCPPResponseService {
         throw new OCPPError(ErrorType.NOT_IMPLEMENTED, `${commandName} is not implemented to handle request response payload ${JSON.stringify(payload, null, 2)}`, commandName);
       }
     } else {
-      throw new OCPPError(ErrorType.SECURITY_ERROR, `The charging station is not registered on the central server. ${commandName} cannot be not issued to handle request response payload ${JSON.stringify(payload, null, 2)}`, commandName);
+      throw new OCPPError(ErrorType.SECURITY_ERROR, `${commandName} cannot be issued to handle request response payload ${JSON.stringify(payload, null, 2)} while the charging station is not registered on the central server. `, commandName);
     }
   }
 
@@ -56,10 +56,12 @@ export default class OCPP16ResponseService extends OCPPResponseService {
       this.chargingStation.addConfigurationKey(OCPP16StandardParametersKey.HeartBeatInterval, payload.interval.toString());
       this.chargingStation.addConfigurationKey(OCPP16StandardParametersKey.HeartbeatInterval, payload.interval.toString(), { visible: false });
       this.chargingStation.heartbeatSetInterval ? this.chargingStation.restartHeartbeat() : this.chargingStation.startHeartbeat();
-    } else if (payload.status === OCPP16RegistrationStatus.PENDING) {
-      logger.info(this.chargingStation.logPrefix() + ' Charging station in pending state on the central server');
+    }
+    if (Object.values(RegistrationStatus).includes(payload.status)) {
+      const logMsg = `${this.chargingStation.logPrefix()} Charging station in '${payload.status}' state on the central server`;
+      payload.status === OCPP16RegistrationStatus.REJECTED ? logger.warn(logMsg) : logger.info(logMsg);
     } else {
-      logger.warn(this.chargingStation.logPrefix() + ' Charging station rejected by the central server');
+      logger.error(this.chargingStation.logPrefix() + ' Charging station boot notification response received: %j with undefined registration status', payload);
     }
   }
 
@@ -181,7 +183,7 @@ export default class OCPP16ResponseService extends OCPPResponseService {
       return;
     }
     if (payload.idTagInfo?.status === OCPP16AuthorizationStatus.ACCEPTED) {
-      (this.chargingStation.getBeginEndMeterValues() && this.chargingStation.getOutOfOrderEndMeterValues())
+      (this.chargingStation.getBeginEndMeterValues() && !this.chargingStation.getOcppStrictCompliance() && this.chargingStation.getOutOfOrderEndMeterValues())
         && await this.chargingStation.ocppRequestService.sendTransactionEndMeterValues(transactionConnectorId, requestPayload.transactionId,
           OCPP16ServiceUtils.buildTransactionEndMeterValue(this.chargingStation, transactionConnectorId, requestPayload.meterStop));
       if (!this.chargingStation.isChargingStationAvailable() || !this.chargingStation.isConnectorAvailable(transactionConnectorId)) {
index 666ee577dec6da25f5043a2e0d8eb116d3ddf79f..ca1363929872f0c9209ca2d1e815fbbd91822845 100644 (file)
@@ -30,10 +30,8 @@ export default abstract class OCPPRequestService {
         skipBufferingOnError: false,
         triggerMessage: false
       }): Promise<JsonType | OCPPError | string> {
-    if (this.chargingStation.isInRejectedState() || (this.chargingStation.isInPendingState() && !params.triggerMessage)) {
-      throw new OCPPError(ErrorType.SECURITY_ERROR, 'Cannot send command payload if the charging station is not in accepted state', commandName);
-    // FIXME: Add template tunable for accepting incoming configuration requests while in unknown state
-    } else if ((this.chargingStation.isInUnknownState() && (commandName === RequestCommand.BOOT_NOTIFICATION || commandName === IncomingRequestCommand.GET_CONFIGURATION || commandName === IncomingRequestCommand.CHANGE_CONFIGURATION || commandName === IncomingRequestCommand.CHANGE_AVAILABILITY || commandName === IncomingRequestCommand.TRIGGER_MESSAGE))
+    if ((this.chargingStation.isInUnknownState() && commandName === RequestCommand.BOOT_NOTIFICATION)
+      || (!this.chargingStation.getOcppStrictCompliance() && this.chargingStation.isInUnknownState())
       || this.chargingStation.isInAcceptedState() || (this.chargingStation.isInPendingState() && params.triggerMessage)) {
       // eslint-disable-next-line @typescript-eslint/no-this-alias
       const self = this;
@@ -108,9 +106,8 @@ export default abstract class OCPPRequestService {
       }), Constants.OCPP_WEBSOCKET_TIMEOUT, new OCPPError(ErrorType.GENERIC_ERROR, `Timeout for message id '${messageId}'`, commandName, messageData?.details as JsonType ?? {}), () => {
         messageType === MessageType.CALL_MESSAGE && this.chargingStation.requests.delete(messageId);
       });
-    } else {
-      throw new OCPPError(ErrorType.SECURITY_ERROR, 'Cannot send command payload if the charging station is in unknown state', commandName);
     }
+    throw new OCPPError(ErrorType.SECURITY_ERROR, `Cannot send command ${commandName} payload when the charging station is in ${this.chargingStation.getRegistrationStatus()} state on the central server`, commandName);
   }
 
   protected handleRequestError(commandName: RequestCommand, error: Error): void {
index c06468e91f6a00c90018725fa900755aa1da8b99..309120b6dff966e77eb18c3065dc13406f878846 100644 (file)
@@ -42,6 +42,7 @@ export default interface ChargingStationTemplate {
   supervisionPassword?: string;
   ocppVersion?: OCPPVersion;
   ocppProtocol?: OCPPProtocol;
+  ocppStrictCompliance?: boolean;
   wsOptions?: ClientOptions & ClientRequestArgs;
   authorizationFile?: string;
   baseName: string;
index e6503d787283a977fdcc19ee3b5bc2857ec2f129..2e43d37697d2e47c603d3b5f312f9d54bef12631 100644 (file)
@@ -29,7 +29,6 @@ export default class Constants {
   static readonly OCPP_WEBSOCKET_TIMEOUT = 60000; // Ms
   static readonly OCPP_TRIGGER_MESSAGE_DELAY = 2000; // Ms
 
-  static readonly CHARGING_STATION_DEFAULT_START_SEQUENCE_DELAY = 60000; // Ms
   static readonly CHARGING_STATION_DEFAULT_RESET_TIME = 60000; // Ms
   static readonly CHARGING_STATION_ATG_INITIALIZATION_TIME = 1000; // Ms
   static readonly CHARGING_STATION_ATG_DEFAULT_STOP_AFTER_HOURS = 0.25; // Hours