X-Git-Url: https://git.piment-noir.org/?a=blobdiff_plain;f=src%2Fcharging-station%2FChargingStation.ts;h=667c154b35595feb376ef14dbe175bfca3aa61cd;hb=8c476a1f1117b85fa29a435fc8733a482472139f;hp=85a944b5e812a7938f5d1c1daa33ae38cfbc2934;hpb=63b48f776a09ac5a5ffb5b161c108e3ca625a3cb;p=e-mobility-charging-stations-simulator.git diff --git a/src/charging-station/ChargingStation.ts b/src/charging-station/ChargingStation.ts index 85a944b5..667c154b 100644 --- a/src/charging-station/ChargingStation.ts +++ b/src/charging-station/ChargingStation.ts @@ -1,10 +1,13 @@ -import { AuthorizationStatus, StartTransactionResponse, StopTransactionReason, StopTransactionResponse } from '../types/ocpp/1.6/Transaction'; +import { AuthorizationStatus, StartTransactionRequest, StartTransactionResponse, StopTransactionReason, StopTransactionRequest, StopTransactionResponse } from '../types/ocpp/1.6/Transaction'; +import { BootNotificationResponse, ChangeConfigurationResponse, DefaultResponse, GetConfigurationResponse, HeartbeatResponse, RegistrationStatus, SetChargingProfileResponse, StatusNotificationResponse, UnlockConnectorResponse } from '../types/ocpp/1.6/RequestResponses'; +import { ChargingProfile, ChargingProfilePurposeType } from '../types/ocpp/1.6/ChargingProfile'; import ChargingStationConfiguration, { ConfigurationKey } from '../types/ChargingStationConfiguration'; import ChargingStationTemplate, { PowerOutType } from '../types/ChargingStationTemplate'; -import { ConfigurationResponse, DefaultRequestResponse, UnlockResponse } from '../types/ocpp/1.6/RequestResponses'; import Connectors, { Connector } from '../types/Connectors'; -import MeterValue, { MeterValueLocation, MeterValueMeasurand, MeterValuePhase, MeterValueUnit } from '../types/ocpp/1.6/MeterValue'; +import { MeterValue, MeterValueLocation, MeterValueMeasurand, MeterValuePhase, MeterValueUnit, MeterValuesRequest, MeterValuesResponse, SampledValue } from '../types/ocpp/1.6/MeterValues'; import { PerformanceObserver, performance } from 'perf_hooks'; +import Requests, { BootNotificationRequest, ChangeConfigurationRequest, GetConfigurationRequest, HeartbeatRequest, RemoteStartTransactionRequest, RemoteStopTransactionRequest, ResetRequest, SetChargingProfileRequest, StatusNotificationRequest, UnlockConnectorRequest } from '../types/ocpp/1.6/Requests'; +import WebSocket, { MessageEvent } from 'ws'; import AutomaticTransactionGenerator from './AutomaticTransactionGenerator'; import { ChargePointErrorCode } from '../types/ocpp/1.6/ChargePointErrorCode'; @@ -15,10 +18,8 @@ import Constants from '../utils/Constants'; import ElectricUtils from '../utils/ElectricUtils'; import MeasurandValues from '../types/MeasurandValues'; import OCPPError from './OcppError'; -import Requests from '../types/ocpp/1.6/Requests'; import Statistics from '../utils/Statistics'; import Utils from '../utils/Utils'; -import WebSocket from 'ws'; import crypto from 'crypto'; import fs from 'fs'; import logger from '../utils/Logger'; @@ -27,13 +28,8 @@ export default class ChargingStation { private _index: number; private _stationTemplateFile: string; private _stationInfo: ChargingStationInfo; - private _bootNotificationMessage: { - chargePointModel: string, - chargePointVendor: string, - chargeBoxSerialNumber?: string, - firmwareVersion?: string, - }; - + private _bootNotificationRequest: BootNotificationRequest; + private _bootNotificationResponse: BootNotificationResponse; private _connectors: Connectors; private _configuration: ChargingStationConfiguration; private _connectorsConfigurationHash: string; @@ -42,15 +38,16 @@ export default class ChargingStation { private _wsConnection: WebSocket; private _hasStopped: boolean; private _hasSocketRestarted: boolean; + private _connectionTimeout: number; private _autoReconnectRetryCount: number; private _autoReconnectMaxRetries: number; - private _autoReconnectTimeout: number; private _requests: Requests; private _messageQueue: string[]; private _automaticTransactionGeneration: AutomaticTransactionGenerator; private _authorizedTags: string[]; private _heartbeatInterval: number; private _heartbeatSetInterval: NodeJS.Timeout; + private _webSocketPingSetInterval: NodeJS.Timeout; private _statistics: Statistics; private _performanceObserver: PerformanceObserver; @@ -63,8 +60,6 @@ export default class ChargingStation { this._hasStopped = false; this._hasSocketRestarted = false; this._autoReconnectRetryCount = 0; - this._autoReconnectMaxRetries = Configuration.getAutoReconnectMaxRetries(); // -1 for unlimited - this._autoReconnectTimeout = Configuration.getAutoReconnectTimeout() * 1000; // Ms, zero for disabling this._requests = {} as Requests; this._messageQueue = [] as string[]; @@ -105,15 +100,17 @@ export default class ChargingStation { _initialize(): void { this._stationInfo = this._buildStationInfo(); - this._bootNotificationMessage = { + this._bootNotificationRequest = { chargePointModel: this._stationInfo.chargePointModel, chargePointVendor: this._stationInfo.chargePointVendor, ...!Utils.isUndefined(this._stationInfo.chargeBoxSerialNumberPrefix) && { chargeBoxSerialNumber: this._stationInfo.chargeBoxSerialNumberPrefix }, ...!Utils.isUndefined(this._stationInfo.firmwareVersion) && { firmwareVersion: this._stationInfo.firmwareVersion }, }; - this._configuration = this._getConfiguration(); + this._configuration = this._getTemplateChargingStationConfiguration(); this._supervisionUrl = this._getSupervisionURL(); this._wsConnectionUrl = this._supervisionUrl + '/' + this._stationInfo.name; + this._connectionTimeout = this._getConnectionTimeout() * 1000; // Ms, zero for disabling + this._autoReconnectMaxRetries = this._getAutoReconnectMaxRetries(); // -1 for unlimited // Build connectors if needed const maxConnectors = this._getMaxNumberOfConnectors(); if (maxConnectors <= 0) { @@ -184,7 +181,7 @@ export default class ChargingStation { return Utils.logPrefix(` ${this._stationInfo.name}:`); } - _getConfiguration(): ChargingStationConfiguration { + _getTemplateChargingStationConfiguration(): ChargingStationConfiguration { return this._stationInfo.Configuration ? this._stationInfo.Configuration : {} as ChargingStationConfiguration; } @@ -243,6 +240,26 @@ export default class ChargingStation { return trxCount; } + _getConnectionTimeout(): number { + if (!Utils.isUndefined(this._stationInfo.connectionTimeout)) { + return this._stationInfo.connectionTimeout; + } + if (!Utils.isUndefined(Configuration.getConnectionTimeout())) { + return Configuration.getConnectionTimeout(); + } + return 30; + } + + _getAutoReconnectMaxRetries(): number { + if (!Utils.isUndefined(this._stationInfo.autoReconnectMaxRetries)) { + return this._stationInfo.autoReconnectMaxRetries; + } + if (!Utils.isUndefined(Configuration.getAutoReconnectMaxRetries())) { + return Configuration.getAutoReconnectMaxRetries(); + } + return -1; + } + _getPowerDivider(): number { let powerDivider = this._getNumberOfConnectors(); if (this._stationInfo.powerSharedByConnectors) { @@ -294,7 +311,7 @@ export default class ChargingStation { return !Utils.isUndefined(this._stationInfo.voltageOut) ? Utils.convertToInt(this._stationInfo.voltageOut) : defaultVoltageOut; } - _getTransactionidTag(transactionId: number): string { + _getTransactionIdTag(transactionId: number): string { for (const connector in this._connectors) { if (this.getConnector(Utils.convertToInt(connector)).transactionId === transactionId) { return this.getConnector(Utils.convertToInt(connector)).idTag; @@ -302,6 +319,14 @@ export default class ChargingStation { } } + _getTransactionMeterStop(transactionId: number): number { + for (const connector in this._connectors) { + if (this.getConnector(Utils.convertToInt(connector)).transactionId === transactionId) { + return this.getConnector(Utils.convertToInt(connector)).lastEnergyActiveImportRegisterValue; + } + } + } + _getPowerOutType(): PowerOutType { return !Utils.isUndefined(this._stationInfo.powerOutType) ? this._stationInfo.powerOutType : PowerOutType.AC; } @@ -321,6 +346,10 @@ export default class ChargingStation { return supervisionUrls as string; } + _getReconnectExponentialDelay(): boolean { + return !Utils.isUndefined(this._stationInfo.reconnectExponentialDelay) ? this._stationInfo.reconnectExponentialDelay : false; + } + _getAuthorizeRemoteTxRequests(): boolean { const authorizeRemoteTxRequests = this._getConfigurationKey('AuthorizeRemoteTxRequests'); return authorizeRemoteTxRequests ? Utils.convertToBoolean(authorizeRemoteTxRequests.value) : false; @@ -331,21 +360,25 @@ export default class ChargingStation { return localAuthListEnabled ? Utils.convertToBoolean(localAuthListEnabled.value) : false; } - _startMessageSequence(): void { + async _startMessageSequence(): Promise { + // Start WebSocket ping + this._startWebSocketPing(); // Start heartbeat this._startHeartbeat(); // Initialize connectors status for (const connector in this._connectors) { - if (!this.getConnector(Utils.convertToInt(connector)).transactionStarted) { - if (!this.getConnector(Utils.convertToInt(connector)).status && this.getConnector(Utils.convertToInt(connector)).bootStatus) { - this.sendStatusNotification(Utils.convertToInt(connector), this.getConnector(Utils.convertToInt(connector)).bootStatus); - } else if (!this._hasStopped && this.getConnector(Utils.convertToInt(connector)).status) { - this.sendStatusNotification(Utils.convertToInt(connector), this.getConnector(Utils.convertToInt(connector)).status); - } else { - this.sendStatusNotification(Utils.convertToInt(connector), ChargePointStatus.AVAILABLE); - } + if (!this._hasStopped && !this.getConnector(Utils.convertToInt(connector)).status && this.getConnector(Utils.convertToInt(connector)).bootStatus) { + // Send status in template at startup + await this.sendStatusNotification(Utils.convertToInt(connector), this.getConnector(Utils.convertToInt(connector)).bootStatus); + } else if (this._hasStopped && this.getConnector(Utils.convertToInt(connector)).bootStatus) { + // Send status in template after reset + await this.sendStatusNotification(Utils.convertToInt(connector), this.getConnector(Utils.convertToInt(connector)).bootStatus); + } else if (!this._hasStopped && this.getConnector(Utils.convertToInt(connector)).status) { + // Send previous status at template reload + await this.sendStatusNotification(Utils.convertToInt(connector), this.getConnector(Utils.convertToInt(connector)).status); } else { - this.sendStatusNotification(Utils.convertToInt(connector), ChargePointStatus.CHARGING); + // Send default status + await this.sendStatusNotification(Utils.convertToInt(connector), ChargePointStatus.AVAILABLE); } } // Start the ATG @@ -363,6 +396,8 @@ export default class ChargingStation { } async _stopMessageSequence(reason: StopTransactionReason = StopTransactionReason.NONE): Promise { + // Stop WebSocket ping + this._stopWebSocketPing(); // Stop heartbeat this._stopHeartbeat(); // Stop the ATG @@ -379,14 +414,46 @@ export default class ChargingStation { } } + _startWebSocketPing(): void { + const webSocketPingInterval: number = this._getConfigurationKey('WebSocketPingInterval') ? Utils.convertToInt(this._getConfigurationKey('WebSocketPingInterval').value) : 0; + if (webSocketPingInterval > 0 && !this._webSocketPingSetInterval) { + this._webSocketPingSetInterval = setInterval(() => { + if (this._wsConnection?.readyState === WebSocket.OPEN) { + this._wsConnection.ping((): void => { }); + } + }, webSocketPingInterval * 1000); + logger.info(this._logPrefix() + ' WebSocket ping started every ' + Utils.secondsToHHMMSS(webSocketPingInterval)); + } else if (this._webSocketPingSetInterval) { + logger.info(this._logPrefix() + ' WebSocket ping every ' + Utils.secondsToHHMMSS(webSocketPingInterval) + ' already started'); + } else { + logger.error(`${this._logPrefix()} WebSocket ping interval set to ${webSocketPingInterval ? Utils.secondsToHHMMSS(webSocketPingInterval) : webSocketPingInterval}, not starting the WebSocket ping`); + } + } + + _stopWebSocketPing(): void { + if (this._webSocketPingSetInterval) { + clearInterval(this._webSocketPingSetInterval); + this._webSocketPingSetInterval = null; + } + } + + _restartWebSocketPing(): void { + // Stop WebSocket ping + this._stopWebSocketPing(); + // Start WebSocket ping + this._startWebSocketPing(); + } + _startHeartbeat(): void { if (this._heartbeatInterval && this._heartbeatInterval > 0 && !this._heartbeatSetInterval) { - this._heartbeatSetInterval = setInterval(() => { - this.sendHeartbeat(); + this._heartbeatSetInterval = setInterval(async () => { + await this.sendHeartbeat(); }, this._heartbeatInterval); logger.info(this._logPrefix() + ' Heartbeat started every ' + Utils.milliSecondsToHHMMSS(this._heartbeatInterval)); + } else if (this._heartbeatSetInterval) { + logger.info(this._logPrefix() + ' Heartbeat every ' + Utils.milliSecondsToHHMMSS(this._heartbeatInterval) + ' already started'); } else { - logger.error(`${this._logPrefix()} Heartbeat interval set to ${Utils.milliSecondsToHHMMSS(this._heartbeatInterval)}, not starting the heartbeat`); + logger.error(`${this._logPrefix()} Heartbeat interval set to ${this._heartbeatInterval ? Utils.milliSecondsToHHMMSS(this._heartbeatInterval) : this._heartbeatInterval}, not starting the heartbeat`); } } @@ -397,6 +464,13 @@ export default class ChargingStation { } } + _restartHeartbeat(): void { + // Stop heartbeat + this._stopHeartbeat(); + // Start heartbeat + this._startHeartbeat(); + } + _startAuthorizationFileMonitoring(): void { // eslint-disable-next-line @typescript-eslint/no-unused-vars fs.watchFile(this._getAuthorizationFile(), (current, previous) => { @@ -421,6 +495,7 @@ export default class ChargingStation { this._automaticTransactionGeneration) { this._automaticTransactionGeneration.stop().catch(() => { }); } + // FIXME?: restart heartbeat and WebSocket ping when their interval values have changed } catch (error) { logger.error(this._logPrefix() + ' Charging station template file monitoring error: %j', error); } @@ -452,12 +527,19 @@ export default class ChargingStation { } } - start(): void { - if (!this._wsConnectionUrl) { - this._wsConnectionUrl = this._supervisionUrl + '/' + this._stationInfo.name; + _openWSConnection(options?: WebSocket.ClientOptions): void { + if (Utils.isUndefined(options)) { + options = {} as WebSocket.ClientOptions; } - this._wsConnection = new WebSocket(this._wsConnectionUrl, 'ocpp' + Constants.OCPP_VERSION_16); + if (Utils.isUndefined(options.handshakeTimeout)) { + options.handshakeTimeout = this._connectionTimeout; + } + this._wsConnection = new WebSocket(this._wsConnectionUrl, 'ocpp' + Constants.OCPP_VERSION_16, options); logger.info(this._logPrefix() + ' Will communicate through URL ' + this._supervisionUrl); + } + + start(): void { + this._openWSConnection(); // Monitor authorization file this._startAuthorizationFileMonitoring(); // Monitor station template file @@ -472,23 +554,28 @@ export default class ChargingStation { this._wsConnection.on('open', this.onOpen.bind(this)); // Handle Socket ping this._wsConnection.on('ping', this.onPing.bind(this)); + // Handle Socket pong + this._wsConnection.on('pong', this.onPong.bind(this)); } async stop(reason: StopTransactionReason = StopTransactionReason.NONE): Promise { - // Stop + // Stop message sequence await this._stopMessageSequence(reason); // eslint-disable-next-line guard-for-in for (const connector in this._connectors) { await this.sendStatusNotification(Utils.convertToInt(connector), ChargePointStatus.UNAVAILABLE); } - if (this._wsConnection && this._wsConnection.readyState === WebSocket.OPEN) { + if (this._wsConnection?.readyState === WebSocket.OPEN) { this._wsConnection.close(); } + this._bootNotificationResponse = null; this._hasStopped = true; } - _reconnect(error): void { - logger.error(this._logPrefix() + ' Socket: abnormally closed %j', error); + async _reconnect(error): Promise { + logger.error(this._logPrefix() + ' Socket: abnormally closed: %j', error); + // Stop heartbeat + this._stopHeartbeat(); // Stop the ATG if needed if (this._stationInfo.AutomaticTransactionGenerator.enable && this._stationInfo.AutomaticTransactionGenerator.stopOnConnectionFailure && @@ -496,32 +583,37 @@ export default class ChargingStation { !this._automaticTransactionGeneration.timeToStop) { this._automaticTransactionGeneration.stop().catch(() => { }); } - // Stop heartbeat - this._stopHeartbeat(); - if (this._autoReconnectTimeout !== 0 && - (this._autoReconnectRetryCount < this._autoReconnectMaxRetries || this._autoReconnectMaxRetries === -1)) { - logger.error(`${this._logPrefix()} Socket: connection retry with timeout ${this._autoReconnectTimeout}ms`); + if (this._autoReconnectRetryCount < this._autoReconnectMaxRetries || this._autoReconnectMaxRetries === -1) { this._autoReconnectRetryCount++; - setTimeout(() => { - logger.error(this._logPrefix() + ' Socket: reconnecting try #' + this._autoReconnectRetryCount.toString()); - this.start(); - }, this._autoReconnectTimeout); - } else if (this._autoReconnectTimeout !== 0 || this._autoReconnectMaxRetries !== -1) { - logger.error(`${this._logPrefix()} Socket: max retries reached (${this._autoReconnectRetryCount}) or retry disabled (${this._autoReconnectTimeout})`); + const reconnectDelay = (this._getReconnectExponentialDelay() ? Utils.exponentialDelay(this._autoReconnectRetryCount) : this._connectionTimeout); + logger.error(`${this._logPrefix()} Socket: connection retry in ${Utils.roundTo(reconnectDelay, 2)}ms, timeout ${reconnectDelay - 100}ms`); + await Utils.sleep(reconnectDelay); + logger.error(this._logPrefix() + ' Socket: reconnecting try #' + this._autoReconnectRetryCount.toString()); + this._openWSConnection({ handshakeTimeout: reconnectDelay - 100 }); + } else if (this._autoReconnectMaxRetries !== -1) { + logger.error(`${this._logPrefix()} Socket: max retries reached (${this._autoReconnectRetryCount}) or retry disabled (${this._autoReconnectMaxRetries})`); } } - onOpen(): void { + async onOpen(): Promise { logger.info(`${this._logPrefix()} Is connected to server through ${this._wsConnectionUrl}`); - if (!this._hasSocketRestarted) { + if (!this._hasSocketRestarted || this._hasStopped) { // Send BootNotification - this.sendBootNotification(); + this._bootNotificationResponse = await this.sendBootNotification(); + } + if (this._bootNotificationResponse.status === RegistrationStatus.ACCEPTED) { + await this._startMessageSequence(); + } else { + do { + await Utils.sleep(this._bootNotificationResponse.interval * 1000); + // Resend BootNotification + this._bootNotificationResponse = await this.sendBootNotification(); + } while (this._bootNotificationResponse.status !== RegistrationStatus.ACCEPTED); } - if (this._hasSocketRestarted) { - this._startMessageSequence(); + if (this._hasSocketRestarted && this._bootNotificationResponse.status === RegistrationStatus.ACCEPTED) { if (!Utils.isEmptyArray(this._messageQueue)) { this._messageQueue.forEach((message, index) => { - if (this._wsConnection && this._wsConnection.readyState === WebSocket.OPEN) { + if (this._wsConnection?.readyState === WebSocket.OPEN) { this._messageQueue.splice(index, 1); this._wsConnection.send(message); } @@ -532,11 +624,11 @@ export default class ChargingStation { this._hasSocketRestarted = false; } - onError(errorEvent): void { - switch (errorEvent) { + async onError(errorEvent): Promise { + switch (errorEvent.code) { case 'ECONNREFUSED': this._hasSocketRestarted = true; - this._reconnect(errorEvent); + await this._reconnect(errorEvent); break; default: logger.error(this._logPrefix() + ' Socket error: %j', errorEvent); @@ -544,16 +636,16 @@ export default class ChargingStation { } } - onClose(closeEvent): void { + async onClose(closeEvent): Promise { switch (closeEvent) { case 1000: // Normal close case 1005: - logger.info(this._logPrefix() + ' Socket normally closed %j', closeEvent); + logger.info(this._logPrefix() + ' Socket normally closed: %j', closeEvent); this._autoReconnectRetryCount = 0; break; default: // Abnormal close this._hasSocketRestarted = true; - this._reconnect(closeEvent); + await this._reconnect(closeEvent); break; } } @@ -562,11 +654,15 @@ export default class ChargingStation { logger.debug(this._logPrefix() + ' Has received a WS ping (rfc6455) from the server'); } - async onMessage(messageEvent): Promise { + onPong(): void { + logger.debug(this._logPrefix() + ' Has received a WS pong (rfc6455) from the server'); + } + + async onMessage(messageEvent: MessageEvent): Promise { let [messageType, messageId, commandName, commandPayload, errorDetails] = [0, '', Constants.ENTITY_CHARGING_STATION, '', '']; try { // Parse the message - [messageType, messageId, commandName, commandPayload, errorDetails] = JSON.parse(messageEvent); + [messageType, messageId, commandName, commandPayload, errorDetails] = JSON.parse(messageEvent.toString()); // Check the Type of message switch (messageType) { @@ -622,15 +718,13 @@ export default class ChargingStation { // Log logger.error('%s Incoming message %j processing error %s on request content type %s', this._logPrefix(), messageEvent, error, this._requests[messageId]); // Send error - await this.sendError(messageId, error, commandName); + messageType !== Constants.OCPP_JSON_CALL_ERROR_MESSAGE && await this.sendError(messageId, error, commandName); } } async sendHeartbeat(): Promise { try { - const payload = { - currentTime: new Date().toISOString(), - }; + const payload: HeartbeatRequest = {}; await this.sendMessage(Utils.generateUUID(), payload, Constants.OCPP_JSON_CALL_MESSAGE, 'Heartbeat'); } catch (error) { logger.error(this._logPrefix() + ' Send Heartbeat error: %j', error); @@ -638,9 +732,9 @@ export default class ChargingStation { } } - async sendBootNotification(): Promise { + async sendBootNotification(): Promise { try { - await this.sendMessage(Utils.generateUUID(), this._bootNotificationMessage, Constants.OCPP_JSON_CALL_MESSAGE, 'BootNotification'); + return await this.sendMessage(Utils.generateUUID(), this._bootNotificationRequest, Constants.OCPP_JSON_CALL_MESSAGE, 'BootNotification') as BootNotificationResponse; } catch (error) { logger.error(this._logPrefix() + ' Send BootNotification error: %j', error); throw error; @@ -650,7 +744,7 @@ export default class ChargingStation { async sendStatusNotification(connectorId: number, status: ChargePointStatus, errorCode: ChargePointErrorCode = ChargePointErrorCode.NO_ERROR): Promise { this.getConnector(connectorId).status = status; try { - const payload = { + const payload: StatusNotificationRequest = { connectorId, errorCode, status, @@ -664,7 +758,7 @@ export default class ChargingStation { async sendStartTransaction(connectorId: number, idTag?: string): Promise { try { - const payload = { + const payload: StartTransactionRequest = { connectorId, ...!Utils.isUndefined(idTag) ? { idTag } : { idTag: Constants.TRANSACTION_DEFAULT_IDTAG }, meterStart: 0, @@ -678,12 +772,12 @@ export default class ChargingStation { } async sendStopTransaction(transactionId: number, reason: StopTransactionReason = StopTransactionReason.NONE): Promise { - const idTag = this._getTransactionidTag(transactionId); + const idTag = this._getTransactionIdTag(transactionId); try { - const payload = { + const payload: StopTransactionRequest = { transactionId, ...!Utils.isUndefined(idTag) && { idTag: idTag }, - meterStop: 0, + meterStop: this._getTransactionMeterStop(transactionId), timestamp: new Date().toISOString(), ...reason && { reason }, }; @@ -697,33 +791,30 @@ export default class ChargingStation { // eslint-disable-next-line consistent-this async sendMeterValues(connectorId: number, interval: number, self: ChargingStation, debug = false): Promise { try { - const sampledValues: { - timestamp: string; - sampledValue: MeterValue[]; - } = { + const meterValue: MeterValue = { timestamp: new Date().toISOString(), sampledValue: [], }; - const meterValuesTemplate = self.getConnector(connectorId).MeterValues; + const meterValuesTemplate: SampledValue[] = self.getConnector(connectorId).MeterValues; for (let index = 0; index < meterValuesTemplate.length; index++) { const connector = self.getConnector(connectorId); // SoC measurand if (meterValuesTemplate[index].measurand && meterValuesTemplate[index].measurand === MeterValueMeasurand.STATE_OF_CHARGE && self._getConfigurationKey('MeterValuesSampledData').value.includes(MeterValueMeasurand.STATE_OF_CHARGE)) { - sampledValues.sampledValue.push({ + meterValue.sampledValue.push({ ...!Utils.isUndefined(meterValuesTemplate[index].unit) ? { unit: meterValuesTemplate[index].unit } : { unit: MeterValueUnit.PERCENT }, ...!Utils.isUndefined(meterValuesTemplate[index].context) && { context: meterValuesTemplate[index].context }, measurand: meterValuesTemplate[index].measurand, ...!Utils.isUndefined(meterValuesTemplate[index].location) ? { location: meterValuesTemplate[index].location } : { location: MeterValueLocation.EV }, ...!Utils.isUndefined(meterValuesTemplate[index].value) ? { value: meterValuesTemplate[index].value } : { value: Utils.getRandomInt(100).toString() }, }); - const sampledValuesIndex = sampledValues.sampledValue.length - 1; - if (Utils.convertToInt(sampledValues.sampledValue[sampledValuesIndex].value) > 100 || debug) { - logger.error(`${self._logPrefix()} MeterValues measurand ${sampledValues.sampledValue[sampledValuesIndex].measurand ? sampledValues.sampledValue[sampledValuesIndex].measurand : MeterValueMeasurand.ENERGY_ACTIVE_IMPORT_REGISTER}: connectorId ${connectorId}, transaction ${connector.transactionId}, value: ${sampledValues.sampledValue[sampledValuesIndex].value}/100`); + const sampledValuesIndex = meterValue.sampledValue.length - 1; + if (Utils.convertToInt(meterValue.sampledValue[sampledValuesIndex].value) > 100 || debug) { + logger.error(`${self._logPrefix()} MeterValues measurand ${meterValue.sampledValue[sampledValuesIndex].measurand ? meterValue.sampledValue[sampledValuesIndex].measurand : MeterValueMeasurand.ENERGY_ACTIVE_IMPORT_REGISTER}: connectorId ${connectorId}, transaction ${connector.transactionId}, value: ${meterValue.sampledValue[sampledValuesIndex].value}/100`); } // Voltage measurand } else if (meterValuesTemplate[index].measurand && meterValuesTemplate[index].measurand === MeterValueMeasurand.VOLTAGE && self._getConfigurationKey('MeterValuesSampledData').value.includes(MeterValueMeasurand.VOLTAGE)) { const voltageMeasurandValue = Utils.getRandomFloatRounded(self._getVoltageOut() + self._getVoltageOut() * 0.1, self._getVoltageOut() - self._getVoltageOut() * 0.1); - sampledValues.sampledValue.push({ + meterValue.sampledValue.push({ ...!Utils.isUndefined(meterValuesTemplate[index].unit) ? { unit: meterValuesTemplate[index].unit } : { unit: MeterValueUnit.VOLT }, ...!Utils.isUndefined(meterValuesTemplate[index].context) && { context: meterValuesTemplate[index].context }, measurand: meterValuesTemplate[index].measurand, @@ -731,14 +822,14 @@ export default class ChargingStation { ...!Utils.isUndefined(meterValuesTemplate[index].value) ? { value: meterValuesTemplate[index].value } : { value: voltageMeasurandValue.toString() }, }); for (let phase = 1; self._getNumberOfPhases() === 3 && phase <= self._getNumberOfPhases(); phase++) { - const voltageValue = Utils.convertToFloat(sampledValues.sampledValue[sampledValues.sampledValue.length - 1].value); + const voltageValue = Utils.convertToFloat(meterValue.sampledValue[meterValue.sampledValue.length - 1].value); let phaseValue: string; if (voltageValue >= 0 && voltageValue <= 250) { phaseValue = `L${phase}-N`; } else if (voltageValue > 250) { phaseValue = `L${phase}-L${(phase + 1) % self._getNumberOfPhases() !== 0 ? (phase + 1) % self._getNumberOfPhases() : self._getNumberOfPhases()}`; } - sampledValues.sampledValue.push({ + meterValue.sampledValue.push({ ...!Utils.isUndefined(meterValuesTemplate[index].unit) ? { unit: meterValuesTemplate[index].unit } : { unit: MeterValueUnit.VOLT }, ...!Utils.isUndefined(meterValuesTemplate[index].context) && { context: meterValuesTemplate[index].context }, measurand: meterValuesTemplate[index].measurand, @@ -785,20 +876,20 @@ export default class ChargingStation { logger.error(errMsg); throw Error(errMsg); } - sampledValues.sampledValue.push({ + meterValue.sampledValue.push({ ...!Utils.isUndefined(meterValuesTemplate[index].unit) ? { unit: meterValuesTemplate[index].unit } : { unit: MeterValueUnit.WATT }, ...!Utils.isUndefined(meterValuesTemplate[index].context) && { context: meterValuesTemplate[index].context }, measurand: meterValuesTemplate[index].measurand, ...!Utils.isUndefined(meterValuesTemplate[index].location) && { location: meterValuesTemplate[index].location }, ...!Utils.isUndefined(meterValuesTemplate[index].value) ? { value: meterValuesTemplate[index].value } : { value: powerMeasurandValues.allPhases.toString() }, }); - const sampledValuesIndex = sampledValues.sampledValue.length - 1; - if (Utils.convertToFloat(sampledValues.sampledValue[sampledValuesIndex].value) > maxPower || debug) { - logger.error(`${self._logPrefix()} MeterValues measurand ${sampledValues.sampledValue[sampledValuesIndex].measurand ? sampledValues.sampledValue[sampledValuesIndex].measurand : MeterValueMeasurand.ENERGY_ACTIVE_IMPORT_REGISTER}: connectorId ${connectorId}, transaction ${connector.transactionId}, value: ${sampledValues.sampledValue[sampledValuesIndex].value}/${maxPower}`); + const sampledValuesIndex = meterValue.sampledValue.length - 1; + if (Utils.convertToFloat(meterValue.sampledValue[sampledValuesIndex].value) > maxPower || debug) { + logger.error(`${self._logPrefix()} MeterValues measurand ${meterValue.sampledValue[sampledValuesIndex].measurand ? meterValue.sampledValue[sampledValuesIndex].measurand : MeterValueMeasurand.ENERGY_ACTIVE_IMPORT_REGISTER}: connectorId ${connectorId}, transaction ${connector.transactionId}, value: ${meterValue.sampledValue[sampledValuesIndex].value}/${maxPower}`); } for (let phase = 1; self._getNumberOfPhases() === 3 && phase <= self._getNumberOfPhases(); phase++) { const phaseValue = `L${phase}-N`; - sampledValues.sampledValue.push({ + meterValue.sampledValue.push({ ...!Utils.isUndefined(meterValuesTemplate[index].unit) ? { unit: meterValuesTemplate[index].unit } : { unit: MeterValueUnit.WATT }, ...!Utils.isUndefined(meterValuesTemplate[index].context) && { context: meterValuesTemplate[index].context }, ...!Utils.isUndefined(meterValuesTemplate[index].measurand) && { measurand: meterValuesTemplate[index].measurand }, @@ -846,20 +937,20 @@ export default class ChargingStation { logger.error(errMsg); throw Error(errMsg); } - sampledValues.sampledValue.push({ + meterValue.sampledValue.push({ ...!Utils.isUndefined(meterValuesTemplate[index].unit) ? { unit: meterValuesTemplate[index].unit } : { unit: MeterValueUnit.AMP }, ...!Utils.isUndefined(meterValuesTemplate[index].context) && { context: meterValuesTemplate[index].context }, measurand: meterValuesTemplate[index].measurand, ...!Utils.isUndefined(meterValuesTemplate[index].location) && { location: meterValuesTemplate[index].location }, ...!Utils.isUndefined(meterValuesTemplate[index].value) ? { value: meterValuesTemplate[index].value } : { value: currentMeasurandValues.allPhases.toString() }, }); - const sampledValuesIndex = sampledValues.sampledValue.length - 1; - if (Utils.convertToFloat(sampledValues.sampledValue[sampledValuesIndex].value) > maxAmperage || debug) { - logger.error(`${self._logPrefix()} MeterValues measurand ${sampledValues.sampledValue[sampledValuesIndex].measurand ? sampledValues.sampledValue[sampledValuesIndex].measurand : MeterValueMeasurand.ENERGY_ACTIVE_IMPORT_REGISTER}: connectorId ${connectorId}, transaction ${connector.transactionId}, value: ${sampledValues.sampledValue[sampledValuesIndex].value}/${maxAmperage}`); + const sampledValuesIndex = meterValue.sampledValue.length - 1; + if (Utils.convertToFloat(meterValue.sampledValue[sampledValuesIndex].value) > maxAmperage || debug) { + logger.error(`${self._logPrefix()} MeterValues measurand ${meterValue.sampledValue[sampledValuesIndex].measurand ? meterValue.sampledValue[sampledValuesIndex].measurand : MeterValueMeasurand.ENERGY_ACTIVE_IMPORT_REGISTER}: connectorId ${connectorId}, transaction ${connector.transactionId}, value: ${meterValue.sampledValue[sampledValuesIndex].value}/${maxAmperage}`); } for (let phase = 1; self._getNumberOfPhases() === 3 && phase <= self._getNumberOfPhases(); phase++) { const phaseValue = `L${phase}`; - sampledValues.sampledValue.push({ + meterValue.sampledValue.push({ ...!Utils.isUndefined(meterValuesTemplate[index].unit) ? { unit: meterValuesTemplate[index].unit } : { unit: MeterValueUnit.AMP }, ...!Utils.isUndefined(meterValuesTemplate[index].context) && { context: meterValuesTemplate[index].context }, ...!Utils.isUndefined(meterValuesTemplate[index].measurand) && { measurand: meterValuesTemplate[index].measurand }, @@ -889,7 +980,7 @@ export default class ChargingStation { connector.lastEnergyActiveImportRegisterValue = 0; } } - sampledValues.sampledValue.push({ + meterValue.sampledValue.push({ ...!Utils.isUndefined(meterValuesTemplate[index].unit) ? { unit: meterValuesTemplate[index].unit } : { unit: MeterValueUnit.WATT_HOUR }, ...!Utils.isUndefined(meterValuesTemplate[index].context) && { context: meterValuesTemplate[index].context }, ...!Utils.isUndefined(meterValuesTemplate[index].measurand) && { measurand: meterValuesTemplate[index].measurand }, @@ -897,21 +988,20 @@ export default class ChargingStation { ...!Utils.isUndefined(meterValuesTemplate[index].value) ? { value: meterValuesTemplate[index].value } : { value: connector.lastEnergyActiveImportRegisterValue.toString() }, }); - const sampledValuesIndex = sampledValues.sampledValue.length - 1; + const sampledValuesIndex = meterValue.sampledValue.length - 1; const maxConsumption = Math.round(self._stationInfo.maxPower * 3600 / (self._stationInfo.powerDivider * interval)); - if (Utils.convertToFloat(sampledValues.sampledValue[sampledValuesIndex].value) > maxConsumption || debug) { - logger.error(`${self._logPrefix()} MeterValues measurand ${sampledValues.sampledValue[sampledValuesIndex].measurand ? sampledValues.sampledValue[sampledValuesIndex].measurand : MeterValueMeasurand.ENERGY_ACTIVE_IMPORT_REGISTER}: connectorId ${connectorId}, transaction ${connector.transactionId}, value: ${sampledValues.sampledValue[sampledValuesIndex].value}/${maxConsumption}`); + if (Utils.convertToFloat(meterValue.sampledValue[sampledValuesIndex].value) > maxConsumption || debug) { + logger.error(`${self._logPrefix()} MeterValues measurand ${meterValue.sampledValue[sampledValuesIndex].measurand ? meterValue.sampledValue[sampledValuesIndex].measurand : MeterValueMeasurand.ENERGY_ACTIVE_IMPORT_REGISTER}: connectorId ${connectorId}, transaction ${connector.transactionId}, value: ${meterValue.sampledValue[sampledValuesIndex].value}/${maxConsumption}`); } // Unsupported measurand } else { logger.info(`${self._logPrefix()} Unsupported MeterValues measurand ${meterValuesTemplate[index].measurand ? meterValuesTemplate[index].measurand : MeterValueMeasurand.ENERGY_ACTIVE_IMPORT_REGISTER} on connectorId ${connectorId}`); } } - - const payload = { + const payload: MeterValuesRequest = { connectorId, transactionId: self.getConnector(connectorId).transactionId, - meterValue: sampledValues, + meterValue: meterValue, }; await self.sendMessage(Utils.generateUUID(), payload, Constants.OCPP_JSON_CALL_MESSAGE, 'MeterValues'); } catch (error) { @@ -931,7 +1021,7 @@ export default class ChargingStation { // eslint-disable-next-line @typescript-eslint/no-this-alias const self = this; // Send a message through wsConnection - return new Promise((resolve, reject) => { + return new Promise((resolve: (value?: any | PromiseLike) => void, reject: (reason?: any) => void) => { let messageToSend; // Type of message switch (messageType) { @@ -953,7 +1043,7 @@ export default class ChargingStation { break; } // Check if wsConnection is ready - if (this._wsConnection && this._wsConnection.readyState === WebSocket.OPEN) { + if (this._wsConnection?.readyState === WebSocket.OPEN) { if (this.getEnableStatistics()) { this._statistics.addMessage(commandName, messageType); } @@ -974,7 +1064,7 @@ export default class ChargingStation { this._messageQueue.push(messageToSend); } // Reject it - return rejectCallback(new OCPPError(commandParams.code ? commandParams.code : Constants.OCPP_ERROR_GENERIC_ERROR, commandParams.message ? commandParams.message : `Web socket closed for message id '${messageId}' with content '${messageToSend}', message buffered`, commandParams.details ? commandParams.details : {})); + return rejectCallback(new OCPPError(commandParams.code ? commandParams.code : Constants.OCPP_ERROR_GENERIC_ERROR, commandParams.message ? commandParams.message : `WebSocket closed for message id '${messageId}' with content '${messageToSend}', message buffered`, commandParams.details ? commandParams.details : {})); } // Response? if (messageType === Constants.OCPP_JSON_CALL_RESULT_MESSAGE) { @@ -1000,7 +1090,7 @@ export default class ChargingStation { if (self.getEnableStatistics()) { self._statistics.addMessage(commandName, messageType); } - logger.debug(`${self._logPrefix()} Error %j occurred when calling command %s with parameters %j`, error, commandName, commandParams); + logger.debug(`${self._logPrefix()} Error: %j occurred when calling command %s with parameters: %j`, error, commandName, commandParams); // Build Exception // eslint-disable-next-line no-empty-function self._requests[messageId] = [() => { }, () => { }, {}]; // Properly format the request @@ -1019,14 +1109,14 @@ export default class ChargingStation { } } - handleResponseBootNotification(payload, requestPayload): void { - if (payload.status === 'Accepted') { - this._heartbeatInterval = payload.interval * 1000; - this._addConfigurationKey('HeartBeatInterval', payload.interval); - this._addConfigurationKey('HeartbeatInterval', payload.interval, false, false); - this._startMessageSequence(); + handleResponseBootNotification(payload: BootNotificationResponse, requestPayload: BootNotificationRequest): void { + if (payload.status === RegistrationStatus.ACCEPTED) { + this._heartbeatInterval = Utils.convertToInt(payload.interval) * 1000; + this._heartbeatSetInterval ? this._restartHeartbeat() : this._startHeartbeat(); + this._addConfigurationKey('HeartBeatInterval', payload.interval.toString()); + this._addConfigurationKey('HeartbeatInterval', payload.interval.toString(), false, false); this._hasStopped && (this._hasStopped = false); - } else if (payload.status === 'Pending') { + } else if (payload.status === RegistrationStatus.PENDING) { logger.info(this._logPrefix() + ' Charging station in pending state on the central server'); } else { logger.info(this._logPrefix() + ' Charging station rejected by the central server'); @@ -1047,10 +1137,10 @@ export default class ChargingStation { } } - handleResponseStartTransaction(payload: StartTransactionResponse, requestPayload): void { + handleResponseStartTransaction(payload: StartTransactionResponse, requestPayload: StartTransactionRequest): void { const connectorId = Utils.convertToInt(requestPayload.connectorId); if (this.getConnector(connectorId).transactionStarted) { - logger.debug(this._logPrefix() + ' Try to start a transaction on an already used connector ' + connectorId.toString() + ': %j', this.getConnector(connectorId)); + logger.debug(this._logPrefix() + ' Trying to start a transaction on an already used connector ' + connectorId.toString() + ': %j', this.getConnector(connectorId)); return; } @@ -1062,7 +1152,7 @@ export default class ChargingStation { } } if (!transactionConnectorId) { - logger.error(this._logPrefix() + ' Try to start a transaction on a non existing connector Id ' + connectorId.toString()); + logger.error(this._logPrefix() + ' Trying to start a transaction on a non existing connector Id ' + connectorId.toString()); return; } if (payload.idTagInfo?.status === AuthorizationStatus.ACCEPTED) { @@ -1079,13 +1169,13 @@ export default class ChargingStation { this._startMeterValues(connectorId, configuredMeterValueSampleInterval ? Utils.convertToInt(configuredMeterValueSampleInterval.value) * 1000 : 60000); } else { - logger.error(this._logPrefix() + ' Starting transaction id ' + payload.transactionId.toString() + ' REJECTED with status ' + payload.idTagInfo?.status + ', idTag ' + requestPayload.idTag); + logger.error(this._logPrefix() + ' Starting transaction id ' + payload.transactionId.toString() + ' REJECTED with status ' + payload.idTagInfo.status + ', idTag ' + requestPayload.idTag); this._resetTransactionOnConnector(connectorId); this.sendStatusNotification(connectorId, ChargePointStatus.AVAILABLE).catch(() => { }); } } - handleResponseStopTransaction(payload: StopTransactionResponse, requestPayload): void { + handleResponseStopTransaction(payload: StopTransactionResponse, requestPayload: StopTransactionRequest): void { let transactionConnectorId: number; for (const connector in this._connectors) { if (this.getConnector(Utils.convertToInt(connector)).transactionId === Utils.convertToInt(requestPayload.transactionId)) { @@ -1094,7 +1184,7 @@ export default class ChargingStation { } } if (!transactionConnectorId) { - logger.error(this._logPrefix() + ' Try to stop a non existing transaction ' + requestPayload.transactionId); + logger.error(this._logPrefix() + ' Trying to stop a non existing transaction ' + requestPayload.transactionId.toString()); return; } if (payload.idTagInfo?.status === AuthorizationStatus.ACCEPTED) { @@ -1102,22 +1192,22 @@ export default class ChargingStation { if (this._stationInfo.powerSharedByConnectors) { this._stationInfo.powerDivider--; } - logger.info(this._logPrefix() + ' Transaction ' + requestPayload.transactionId + ' STOPPED on ' + this._stationInfo.name + '#' + transactionConnectorId.toString()); + logger.info(this._logPrefix() + ' Transaction ' + requestPayload.transactionId.toString() + ' STOPPED on ' + this._stationInfo.name + '#' + transactionConnectorId.toString()); this._resetTransactionOnConnector(transactionConnectorId); } else { - logger.error(this._logPrefix() + ' Stopping transaction id ' + requestPayload.transactionId + ' REJECTED with status ' + payload.idTagInfo?.status); + logger.error(this._logPrefix() + ' Stopping transaction id ' + requestPayload.transactionId.toString() + ' REJECTED with status ' + payload.idTagInfo?.status); } } - handleResponseStatusNotification(payload, requestPayload): void { + handleResponseStatusNotification(payload: StatusNotificationRequest, requestPayload: StatusNotificationResponse): void { logger.debug(this._logPrefix() + ' Status notification response received: %j to StatusNotification request: %j', payload, requestPayload); } - handleResponseMeterValues(payload, requestPayload): void { + handleResponseMeterValues(payload: MeterValuesRequest, requestPayload: MeterValuesResponse): void { logger.debug(this._logPrefix() + ' MeterValues response received: %j to MeterValues request: %j', payload, requestPayload); } - handleResponseHeartbeat(payload, requestPayload): void { + handleResponseHeartbeat(payload: HeartbeatResponse, requestPayload: HeartbeatRequest): void { logger.debug(this._logPrefix() + ' Heartbeat response received: %j to Heartbeat request: %j', payload, requestPayload); } @@ -1145,7 +1235,7 @@ export default class ChargingStation { } // Simulate charging station restart - handleRequestReset(commandPayload): DefaultRequestResponse { + handleRequestReset(commandPayload: ResetRequest): DefaultResponse { setImmediate(async () => { await this.stop(commandPayload.type + 'Reset' as StopTransactionReason); await Utils.sleep(this._stationInfo.resetTime); @@ -1155,10 +1245,14 @@ export default class ChargingStation { return Constants.OCPP_RESPONSE_ACCEPTED; } - async handleRequestUnlockConnector(commandPayload): Promise { + handleRequestClearCache(): DefaultResponse { + return Constants.OCPP_RESPONSE_ACCEPTED; + } + + async handleRequestUnlockConnector(commandPayload: UnlockConnectorRequest): Promise { const connectorId = Utils.convertToInt(commandPayload.connectorId); if (connectorId === 0) { - logger.error(this._logPrefix() + ' Try to unlock connector ' + connectorId.toString()); + logger.error(this._logPrefix() + ' Trying to unlock connector ' + connectorId.toString()); return Constants.OCPP_RESPONSE_UNLOCK_NOT_SUPPORTED; } if (this.getConnector(connectorId).transactionStarted) { @@ -1197,7 +1291,7 @@ export default class ChargingStation { } } - handleRequestGetConfiguration(commandPayload): { configurationKey: ConfigurationKey[]; unknownKey: string[] } { + handleRequestGetConfiguration(commandPayload: GetConfigurationRequest): GetConfigurationResponse { const configurationKey: ConfigurationKey[] = []; const unknownKey: string[] = []; if (Utils.isEmptyArray(commandPayload.key)) { @@ -1215,7 +1309,7 @@ export default class ChargingStation { }); } } else { - for (const key of commandPayload.key as string[]) { + for (const key of commandPayload.key) { const keyFound = this._getConfigurationKey(key); if (keyFound) { if (Utils.isUndefined(keyFound.visible)) { @@ -1240,7 +1334,7 @@ export default class ChargingStation { }; } - handleRequestChangeConfiguration(commandPayload): ConfigurationResponse { + handleRequestChangeConfiguration(commandPayload: ChangeConfigurationRequest): ChangeConfigurationResponse { const keyToChange = this._getConfigurationKey(commandPayload.key); if (!keyToChange) { return Constants.OCPP_CONFIGURATION_RESPONSE_NOT_SUPPORTED; @@ -1248,22 +1342,26 @@ export default class ChargingStation { return Constants.OCPP_CONFIGURATION_RESPONSE_REJECTED; } else if (keyToChange && !keyToChange.readonly) { const keyIndex = this._configuration.configurationKey.indexOf(keyToChange); - this._configuration.configurationKey[keyIndex].value = commandPayload.value; + let valueChanged = false; + if (this._configuration.configurationKey[keyIndex].value !== commandPayload.value) { + this._configuration.configurationKey[keyIndex].value = commandPayload.value; + valueChanged = true; + } let triggerHeartbeatRestart = false; - if (keyToChange.key === 'HeartBeatInterval') { + if (keyToChange.key === 'HeartBeatInterval' && valueChanged) { this._setConfigurationKeyValue('HeartbeatInterval', commandPayload.value); triggerHeartbeatRestart = true; } - if (keyToChange.key === 'HeartbeatInterval') { + if (keyToChange.key === 'HeartbeatInterval' && valueChanged) { this._setConfigurationKeyValue('HeartBeatInterval', commandPayload.value); triggerHeartbeatRestart = true; } if (triggerHeartbeatRestart) { this._heartbeatInterval = Utils.convertToInt(commandPayload.value) * 1000; - // Stop heartbeat - this._stopHeartbeat(); - // Start heartbeat - this._startHeartbeat(); + this._restartHeartbeat(); + } + if (keyToChange.key === 'WebSocketPingInterval' && valueChanged) { + this._restartWebSocketPing(); } if (keyToChange.reboot) { return Constants.OCPP_CONFIGURATION_RESPONSE_REBOOT_REQUIRED; @@ -1272,7 +1370,26 @@ export default class ChargingStation { } } - async handleRequestRemoteStartTransaction(commandPayload): Promise { + handleRequestSetChargingProfile(commandPayload: SetChargingProfileRequest): SetChargingProfileResponse { + if (!this.getConnector(commandPayload.connectorId)) { + logger.error(`${this._logPrefix()} Trying to set a charging profile to a non existing connector Id ${commandPayload.connectorId}`); + return Constants.OCPP_CHARGING_PROFILE_RESPONSE_REJECTED; + } + if (commandPayload.csChargingProfiles.chargingProfilePurpose === ChargingProfilePurposeType.TX_PROFILE && !this.getConnector(commandPayload.connectorId)?.transactionStarted) { + return Constants.OCPP_CHARGING_PROFILE_RESPONSE_REJECTED; + } + this.getConnector(commandPayload.connectorId).chargingProfiles.forEach((chargingProfile: ChargingProfile, index: number) => { + if (chargingProfile.chargingProfileId === commandPayload.csChargingProfiles.chargingProfileId + || (chargingProfile.stackLevel === commandPayload.csChargingProfiles.stackLevel && chargingProfile.chargingProfilePurpose === commandPayload.csChargingProfiles.chargingProfilePurpose)) { + this.getConnector(commandPayload.connectorId).chargingProfiles[index] = chargingProfile; + return Constants.OCPP_CHARGING_PROFILE_RESPONSE_ACCEPTED; + } + }); + this.getConnector(commandPayload.connectorId).chargingProfiles.push(commandPayload.csChargingProfiles); + return Constants.OCPP_CHARGING_PROFILE_RESPONSE_ACCEPTED; + } + + async handleRequestRemoteStartTransaction(commandPayload: RemoteStartTransactionRequest): Promise { const transactionConnectorID: number = commandPayload.connectorId ? Utils.convertToInt(commandPayload.connectorId) : 1; if (this._getAuthorizeRemoteTxRequests() && this._getLocalAuthListEnabled() && this.hasAuthorizedTags()) { // Check if authorized @@ -1282,7 +1399,7 @@ export default class ChargingStation { logger.debug(this._logPrefix() + ' Transaction remotely STARTED on ' + this._stationInfo.name + '#' + transactionConnectorID.toString() + ' for idTag ' + commandPayload.idTag); return Constants.OCPP_RESPONSE_ACCEPTED; } - logger.error(this._logPrefix() + ' Remote starting transaction REJECTED with status ' + commandPayload.idTagInfo?.status + ', idTag ' + commandPayload.idTag); + logger.error(this._logPrefix() + ' Remote starting transaction REJECTED, idTag ' + commandPayload.idTag); return Constants.OCPP_RESPONSE_REJECTED; } // No local authorization check required => start transaction @@ -1291,7 +1408,7 @@ export default class ChargingStation { return Constants.OCPP_RESPONSE_ACCEPTED; } - async handleRequestRemoteStopTransaction(commandPayload): Promise { + async handleRequestRemoteStopTransaction(commandPayload: RemoteStopTransactionRequest): Promise { const transactionId = Utils.convertToInt(commandPayload.transactionId); for (const connector in this._connectors) { if (this.getConnector(Utils.convertToInt(connector)).transactionId === transactionId) { @@ -1299,7 +1416,7 @@ export default class ChargingStation { return Constants.OCPP_RESPONSE_ACCEPTED; } } - logger.info(this._logPrefix() + ' Try to stop remotely a non existing transaction ' + transactionId.toString()); + logger.info(this._logPrefix() + ' Trying to remote stop a non existing transaction ' + transactionId.toString()); return Constants.OCPP_RESPONSE_REJECTED; } }