From caad9d6b03dbfc507da6d8e79ccbbaf74593e981 Mon Sep 17 00:00:00 2001 From: =?utf8?q?J=C3=A9r=C3=B4me=20Benoit?= Date: Fri, 4 Feb 2022 23:47:09 +0100 Subject: [PATCH] Fix request and response handling in all registration state MIME-Version: 1.0 Content-Type: text/plain; charset=utf8 Content-Transfer-Encoding: 8bit Signed-off-by: Jérôme Benoit --- .../AutomaticTransactionGenerator.ts | 2 +- src/charging-station/ChargingStation.ts | 12 +- .../ocpp/1.6/OCPP16IncomingRequestService.ts | 14 +- .../ocpp/1.6/OCPP16RequestService.ts | 13 +- .../ocpp/OCPPRequestService.ts | 154 ++++++++++-------- src/types/ocpp/Requests.ts | 5 + 6 files changed, 110 insertions(+), 90 deletions(-) diff --git a/src/charging-station/AutomaticTransactionGenerator.ts b/src/charging-station/AutomaticTransactionGenerator.ts index 3f5b03c1..dddfc045 100644 --- a/src/charging-station/AutomaticTransactionGenerator.ts +++ b/src/charging-station/AutomaticTransactionGenerator.ts @@ -67,7 +67,7 @@ export default class AutomaticTransactionGenerator { break; } if (!this.chargingStation.isInAcceptedState()) { - logger.error(this.logPrefix(connectorId) + ' entered in transaction loop while the charging station is in accepted state'); + logger.error(this.logPrefix(connectorId) + ' entered in transaction loop while the charging station is not in accepted state'); this.stopConnector(connectorId); break; } diff --git a/src/charging-station/ChargingStation.ts b/src/charging-station/ChargingStation.ts index 018a6fbe..6be15b83 100644 --- a/src/charging-station/ChargingStation.ts +++ b/src/charging-station/ChargingStation.ts @@ -663,15 +663,15 @@ export default class ChargingStation { 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 + // 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 this.startMessageSequence(); - this.stopped && (this.stopped = false); - if (this.wsConnectionRestarted && this.isWebSocketConnectionOpened()) { - this.flushMessageBuffer(); - } 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 81ff70b7..77bf637f 100644 --- a/src/charging-station/ocpp/1.6/OCPP16IncomingRequestService.ts +++ b/src/charging-station/ocpp/1.6/OCPP16IncomingRequestService.ts @@ -47,9 +47,11 @@ export default class OCPP16IncomingRequestService extends OCPPIncomingRequestSer public async handleRequest(messageId: string, commandName: OCPP16IncomingRequestCommand, commandPayload: Record): Promise { let result: Record; - if (this.chargingStation.isRegistered() && - !(this.chargingStation.isInPendingState - && (commandName === OCPP16IncomingRequestCommand.REMOTE_START_TRANSACTION || commandName === OCPP16IncomingRequestCommand.REMOTE_STOP_TRANSACTION))) { + 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.isRegistered()) { if (this.incomingRequestHandlers.has(commandName)) { try { // Call the method to build the result @@ -64,7 +66,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 not issued to handle request payload ${JSON.stringify(commandPayload, null, 2)}`, commandName); + 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); } // Send the built result await this.chargingStation.ocppRequestService.sendResult(messageId, result, commandName); @@ -431,12 +433,12 @@ export default class OCPP16IncomingRequestService extends OCPPIncomingRequestSer setTimeout(() => { this.chargingStation.ocppRequestService.sendBootNotification(this.chargingStation.getBootNotificationRequest().chargePointModel, this.chargingStation.getBootNotificationRequest().chargePointVendor, this.chargingStation.getBootNotificationRequest().chargeBoxSerialNumber, - this.chargingStation.getBootNotificationRequest().firmwareVersion).catch(() => { /* This is intentional */ }); + this.chargingStation.getBootNotificationRequest().firmwareVersion, null, null, null, null, null, { triggerMessage: true }).catch(() => { /* This is intentional */ }); }, Constants.OCPP_TRIGGER_MESSAGE_DELAY); return Constants.OCPP_TRIGGER_MESSAGE_RESPONSE_ACCEPTED; case MessageTrigger.Heartbeat: setTimeout(() => { - this.chargingStation.ocppRequestService.sendHeartbeat().catch(() => { /* This is intentional */ }); + this.chargingStation.ocppRequestService.sendHeartbeat({ triggerMessage: true }).catch(() => { /* This is intentional */ }); }, Constants.OCPP_TRIGGER_MESSAGE_DELAY); return Constants.OCPP_TRIGGER_MESSAGE_RESPONSE_ACCEPTED; default: diff --git a/src/charging-station/ocpp/1.6/OCPP16RequestService.ts b/src/charging-station/ocpp/1.6/OCPP16RequestService.ts index f4f1782e..6ae6cf32 100644 --- a/src/charging-station/ocpp/1.6/OCPP16RequestService.ts +++ b/src/charging-station/ocpp/1.6/OCPP16RequestService.ts @@ -18,21 +18,23 @@ import { OCPP16DiagnosticsStatus } from '../../../types/ocpp/1.6/DiagnosticsStat import { OCPP16ServiceUtils } from './OCPP16ServiceUtils'; import OCPPError from '../../../exception/OCPPError'; import OCPPRequestService from '../OCPPRequestService'; +import { SendParams } from '../../../types/ocpp/Requests'; import Utils from '../../../utils/Utils'; import logger from '../../../utils/Logger'; export default class OCPP16RequestService extends OCPPRequestService { - public async sendHeartbeat(): Promise { + public async sendHeartbeat(params?: SendParams): Promise { try { const payload: HeartbeatRequest = {}; - await this.sendMessage(Utils.generateUUID(), payload, MessageType.CALL_MESSAGE, OCPP16RequestCommand.HEARTBEAT); + await this.sendMessage(Utils.generateUUID(), payload, MessageType.CALL_MESSAGE, OCPP16RequestCommand.HEARTBEAT, params); } catch (error) { this.handleRequestError(OCPP16RequestCommand.HEARTBEAT, error as Error); } } public async sendBootNotification(chargePointModel: string, chargePointVendor: string, chargeBoxSerialNumber?: string, firmwareVersion?: string, - chargePointSerialNumber?: string, iccid?: string, imsi?: string, meterSerialNumber?: string, meterType?: string): Promise { + chargePointSerialNumber?: string, iccid?: string, imsi?: string, meterSerialNumber?: string, meterType?: string, + params?: SendParams): Promise { try { const payload: OCPP16BootNotificationRequest = { chargePointModel, @@ -45,7 +47,8 @@ export default class OCPP16RequestService extends OCPPRequestService { ...!Utils.isUndefined(meterSerialNumber) && { meterSerialNumber }, ...!Utils.isUndefined(meterType) && { meterType } }; - return await this.sendMessage(Utils.generateUUID(), payload, MessageType.CALL_MESSAGE, OCPP16RequestCommand.BOOT_NOTIFICATION, true) as OCPP16BootNotificationResponse; + return await this.sendMessage(Utils.generateUUID(), payload, MessageType.CALL_MESSAGE, + OCPP16RequestCommand.BOOT_NOTIFICATION, { ...params, skipBufferingOnError: true }) as OCPP16BootNotificationResponse; } catch (error) { this.handleRequestError(OCPP16RequestCommand.BOOT_NOTIFICATION, error as Error); } @@ -316,7 +319,7 @@ export default class OCPP16RequestService extends OCPPRequestService { : Utils.getRandomFloatRounded(maxEnergyRounded); // Persist previous value on connector if (connector && !Utils.isNullOrUndefined(connector.energyActiveImportRegisterValue) && connector.energyActiveImportRegisterValue >= 0 && - !Utils.isNullOrUndefined(connector.transactionEnergyActiveImportRegisterValue) && connector.transactionEnergyActiveImportRegisterValue >= 0) { + !Utils.isNullOrUndefined(connector.transactionEnergyActiveImportRegisterValue) && connector.transactionEnergyActiveImportRegisterValue >= 0) { connector.energyActiveImportRegisterValue += energyValueRounded; connector.transactionEnergyActiveImportRegisterValue += energyValueRounded; } else { diff --git a/src/charging-station/ocpp/OCPPRequestService.ts b/src/charging-station/ocpp/OCPPRequestService.ts index b29c0451..0bfa2b22 100644 --- a/src/charging-station/ocpp/OCPPRequestService.ts +++ b/src/charging-station/ocpp/OCPPRequestService.ts @@ -1,5 +1,5 @@ import { AuthorizeResponse, StartTransactionResponse, StopTransactionReason, StopTransactionResponse } from '../../types/ocpp/Transaction'; -import { DiagnosticsStatus, IncomingRequestCommand, RequestCommand } from '../../types/ocpp/Requests'; +import { DiagnosticsStatus, IncomingRequestCommand, RequestCommand, SendParams } from '../../types/ocpp/Requests'; import { BootNotificationResponse } from '../../types/ocpp/Responses'; import { ChargePointErrorCode } from '../../types/ocpp/ChargePointErrorCode'; @@ -25,80 +25,90 @@ export default abstract class OCPPRequestService { } public async sendMessage(messageId: string, messageData: any, messageType: MessageType, commandName: RequestCommand | IncomingRequestCommand, - skipBufferingOnError = false): Promise { - // eslint-disable-next-line @typescript-eslint/no-this-alias - const self = this; - // Send a message through wsConnection - return Utils.promiseWithTimeout(new Promise((resolve, reject) => { - const messageToSend = this.buildMessageToSend(messageId, messageData, messageType, commandName, responseCallback, rejectCallback); - if (this.chargingStation.getEnableStatistics()) { - this.chargingStation.performanceStatistics.addRequestStatistic(commandName, messageType); - } - // Check if wsConnection opened - if (this.chargingStation.isWebSocketConnectionOpened()) { - // Yes: Send Message - const beginId = PerformanceStatistics.beginMeasure(commandName); - // FIXME: Handle sending error - this.chargingStation.wsConnection.send(messageToSend); - PerformanceStatistics.endMeasure(commandName, beginId); - } else if (!skipBufferingOnError) { - // Buffer it - this.chargingStation.bufferMessage(messageToSend); - const ocppError = new OCPPError(ErrorType.GENERIC_ERROR, `WebSocket closed for buffered message id '${messageId}' with content '${messageToSend}'`, messageData?.details ?? {}); - if (messageType === MessageType.CALL_MESSAGE) { - // Reject it but keep the request in the cache - return reject(ocppError); + params: SendParams = { + skipBufferingOnError: false, + triggerMessage: false + }): Promise { + if ((this.chargingStation.isInPendingState() && !params.triggerMessage) || this.chargingStation.isInRejectedState()) { + throw new OCPPError(ErrorType.SECURITY_ERROR, 'Cannot send command payload if the charging station is not in accepted state', commandName); + } else if (this.chargingStation.isInAcceptedState() || (this.chargingStation.isInPendingState() && params.triggerMessage)) { + // eslint-disable-next-line @typescript-eslint/no-this-alias + const self = this; + // Send a message through wsConnection + return Utils.promiseWithTimeout(new Promise((resolve, reject) => { + const messageToSend = this.buildMessageToSend(messageId, messageData, messageType, commandName, responseCallback, rejectCallback); + if (this.chargingStation.getEnableStatistics()) { + this.chargingStation.performanceStatistics.addRequestStatistic(commandName, messageType); } - return rejectCallback(ocppError, false); - } else { - // Reject it - return rejectCallback(new OCPPError(ErrorType.GENERIC_ERROR, `WebSocket closed for non buffered message id '${messageId}' with content '${messageToSend}'`, messageData?.details ?? {}), false); - } - // Response? - if (messageType !== MessageType.CALL_MESSAGE) { - // Yes: send Ok - return resolve(messageData); - } - - /** - * Function that will receive the request's response - * - * @param payload - * @param requestPayload - */ - async function responseCallback(payload: Record | string, requestPayload: Record): Promise { - if (self.chargingStation.getEnableStatistics()) { - self.chargingStation.performanceStatistics.addRequestStatistic(commandName, MessageType.CALL_RESULT_MESSAGE); + // Check if wsConnection opened + if (this.chargingStation.isWebSocketConnectionOpened()) { + // Yes: Send Message + const beginId = PerformanceStatistics.beginMeasure(commandName); + // FIXME: Handle sending error + this.chargingStation.wsConnection.send(messageToSend); + PerformanceStatistics.endMeasure(commandName, beginId); + } else if (!params.skipBufferingOnError) { + // Buffer it + this.chargingStation.bufferMessage(messageToSend); + const ocppError = new OCPPError(ErrorType.GENERIC_ERROR, `WebSocket closed for buffered message id '${messageId}' with content '${messageToSend}'`, messageData?.details ?? {}); + if (messageType === MessageType.CALL_MESSAGE) { + // Reject it but keep the request in the cache + return reject(ocppError); + } + return rejectCallback(ocppError, false); + } else { + // Reject it + return rejectCallback(new OCPPError(ErrorType.GENERIC_ERROR, `WebSocket closed for non buffered message id '${messageId}' with content '${messageToSend}'`, messageData?.details ?? {}), false); } - // Handle the request's response - try { - await self.ocppResponseService.handleResponse(commandName as RequestCommand, payload, requestPayload); - resolve(payload); - } catch (error) { - reject(error); - throw error; - } finally { - self.chargingStation.requests.delete(messageId); + // Response? + if (messageType !== MessageType.CALL_MESSAGE) { + // Yes: send Ok + return resolve(messageData); } - } - /** - * Function that will receive the request's error response - * - * @param error - * @param requestStatistic - */ - function rejectCallback(error: OCPPError, requestStatistic = true): void { - if (requestStatistic && self.chargingStation.getEnableStatistics()) { - self.chargingStation.performanceStatistics.addRequestStatistic(commandName, MessageType.CALL_ERROR_MESSAGE); + + /** + * Function that will receive the request's response + * + * @param payload + * @param requestPayload + */ + async function responseCallback(payload: Record | string, requestPayload: Record): Promise { + if (self.chargingStation.getEnableStatistics()) { + self.chargingStation.performanceStatistics.addRequestStatistic(commandName, MessageType.CALL_RESULT_MESSAGE); + } + // Handle the request's response + try { + await self.ocppResponseService.handleResponse(commandName as RequestCommand, payload, requestPayload); + resolve(payload); + } catch (error) { + reject(error); + throw error; + } finally { + self.chargingStation.requests.delete(messageId); + } } - logger.error(`${self.chargingStation.logPrefix()} Error %j occurred when calling command %s with message data %j`, error, commandName, messageData); - self.chargingStation.requests.delete(messageId); - reject(error); - } - }), Constants.OCPP_WEBSOCKET_TIMEOUT, new OCPPError(ErrorType.GENERIC_ERROR, `Timeout for message id '${messageId}'`, messageData?.details ?? {}), () => { - messageType === MessageType.CALL_MESSAGE && this.chargingStation.requests.delete(messageId); - }); + + /** + * Function that will receive the request's error response + * + * @param error + * @param requestStatistic + */ + function rejectCallback(error: OCPPError, requestStatistic = true): void { + if (requestStatistic && self.chargingStation.getEnableStatistics()) { + self.chargingStation.performanceStatistics.addRequestStatistic(commandName, MessageType.CALL_ERROR_MESSAGE); + } + logger.error(`${self.chargingStation.logPrefix()} Error %j occurred when calling command %s with message data %j`, error, commandName, messageData); + self.chargingStation.requests.delete(messageId); + reject(error); + } + }), Constants.OCPP_WEBSOCKET_TIMEOUT, new OCPPError(ErrorType.GENERIC_ERROR, `Timeout for message id '${messageId}'`, messageData?.details ?? {}), () => { + 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, { status: this.chargingStation?.bootNotificationResponse?.status }); + } } protected handleRequestError(commandName: RequestCommand, error: Error): void { @@ -132,8 +142,8 @@ export default abstract class OCPPRequestService { return messageToSend; } - public abstract sendHeartbeat(): Promise; - public abstract sendBootNotification(chargePointModel: string, chargePointVendor: string, chargeBoxSerialNumber?: string, firmwareVersion?: string, chargePointSerialNumber?: string, iccid?: string, imsi?: string, meterSerialNumber?: string, meterType?: string): Promise; + public abstract sendHeartbeat(params?: SendParams): Promise; + public abstract sendBootNotification(chargePointModel: string, chargePointVendor: string, chargeBoxSerialNumber?: string, firmwareVersion?: string, chargePointSerialNumber?: string, iccid?: string, imsi?: string, meterSerialNumber?: string, meterType?: string, params?: SendParams): Promise; public abstract sendStatusNotification(connectorId: number, status: ChargePointStatus, errorCode?: ChargePointErrorCode): Promise; public abstract sendAuthorize(connectorId: number, idTag?: string): Promise; public abstract sendStartTransaction(connectorId: number, idTag?: string): Promise; diff --git a/src/types/ocpp/Requests.ts b/src/types/ocpp/Requests.ts index 2808e2f5..7f295584 100644 --- a/src/types/ocpp/Requests.ts +++ b/src/types/ocpp/Requests.ts @@ -4,6 +4,11 @@ import { MessageType } from './MessageType'; import { OCPP16DiagnosticsStatus } from './1.6/DiagnosticsStatus'; import OCPPError from '../../exception/OCPPError'; +export interface SendParams { + skipBufferingOnError?: boolean, + triggerMessage?: boolean +} + export type IncomingRequestHandler = (commandPayload: Record) => Record | Promise>; export type BootNotificationRequest = OCPP16BootNotificationRequest; -- 2.34.1