From 672fed6e70e94e37ba8db689d8517f42ae0f4477 Mon Sep 17 00:00:00 2001 From: =?utf8?q?J=C3=A9r=C3=B4me=20Benoit?= Date: Wed, 9 Feb 2022 18:00:44 +0100 Subject: [PATCH] Add occpStrictCompliance template tunable MIME-Version: 1.0 Content-Type: text/plain; charset=utf8 Content-Transfer-Encoding: 8bit Signed-off-by: Jérôme Benoit --- README.md | 5 ++-- src/charging-station/ChargingStation.ts | 28 +++++++++---------- .../ocpp/1.6/OCPP16IncomingRequestService.ts | 11 ++++---- .../ocpp/1.6/OCPP16RequestService.ts | 2 +- .../ocpp/1.6/OCPP16ResponseService.ts | 14 ++++++---- .../ocpp/OCPPRequestService.ts | 9 ++---- src/types/ChargingStationTemplate.ts | 1 + src/utils/Constants.ts | 1 - 8 files changed, 34 insertions(+), 37 deletions(-) diff --git a/README.md b/README.md index 9f1156e7..29bf46dd 100644 --- 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 diff --git a/src/charging-station/ChargingStation.ts b/src/charging-station/ChargingStation.ts index 48833337..8d2efa75 100644 --- a/src/charging-station/ChargingStation.ts +++ b/src/charging-station/ChargingStation.ts @@ -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 { 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()})`); } diff --git a/src/charging-station/ocpp/1.6/OCPP16IncomingRequestService.ts b/src/charging-station/ocpp/1.6/OCPP16IncomingRequestService.ts index 72e4d316..c9f9042f 100644 --- a/src/charging-station/ocpp/1.6/OCPP16IncomingRequestService.ts +++ b/src/charging-station/ocpp/1.6/OCPP16IncomingRequestService.ts @@ -48,12 +48,11 @@ export default class OCPP16IncomingRequestService extends OCPPIncomingRequestSer public async handleRequest(messageId: string, commandName: OCPP16IncomingRequestCommand, commandPayload: JsonType): Promise { 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); diff --git a/src/charging-station/ocpp/1.6/OCPP16RequestService.ts b/src/charging-station/ocpp/1.6/OCPP16RequestService.ts index 1fc89fa8..3580cc52 100644 --- a/src/charging-station/ocpp/1.6/OCPP16RequestService.ts +++ b/src/charging-station/ocpp/1.6/OCPP16RequestService.ts @@ -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, diff --git a/src/charging-station/ocpp/1.6/OCPP16ResponseService.ts b/src/charging-station/ocpp/1.6/OCPP16ResponseService.ts index 05e2ed9a..23751c33 100644 --- a/src/charging-station/ocpp/1.6/OCPP16ResponseService.ts +++ b/src/charging-station/ocpp/1.6/OCPP16ResponseService.ts @@ -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)) { diff --git a/src/charging-station/ocpp/OCPPRequestService.ts b/src/charging-station/ocpp/OCPPRequestService.ts index 666ee577..ca136392 100644 --- a/src/charging-station/ocpp/OCPPRequestService.ts +++ b/src/charging-station/ocpp/OCPPRequestService.ts @@ -30,10 +30,8 @@ export default abstract class OCPPRequestService { skipBufferingOnError: false, triggerMessage: false }): Promise { - 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 { diff --git a/src/types/ChargingStationTemplate.ts b/src/types/ChargingStationTemplate.ts index c06468e9..309120b6 100644 --- a/src/types/ChargingStationTemplate.ts +++ b/src/types/ChargingStationTemplate.ts @@ -42,6 +42,7 @@ export default interface ChargingStationTemplate { supervisionPassword?: string; ocppVersion?: OCPPVersion; ocppProtocol?: OCPPProtocol; + ocppStrictCompliance?: boolean; wsOptions?: ClientOptions & ClientRequestArgs; authorizationFile?: string; baseName: string; diff --git a/src/utils/Constants.ts b/src/utils/Constants.ts index e6503d78..2e43d376 100644 --- a/src/utils/Constants.ts +++ b/src/utils/Constants.ts @@ -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 -- 2.34.1