X-Git-Url: https://git.piment-noir.org/?a=blobdiff_plain;f=src%2Fcharging-station%2FChargingStation.ts;h=ada1c4b9e19c743c7fbcbbee7ca5925d5aaf1a88;hb=3bf051e2a6a3bd41b4ffe6ccffdcc05c2ff509bc;hp=b63bd2c20ec15d96eb15a6b86512522b2855cd6a;hpb=032d6efcb5be418f04d38c39533e7d73d0337195;p=e-mobility-charging-stations-simulator.git diff --git a/src/charging-station/ChargingStation.ts b/src/charging-station/ChargingStation.ts index b63bd2c2..ada1c4b9 100644 --- a/src/charging-station/ChargingStation.ts +++ b/src/charging-station/ChargingStation.ts @@ -1,10 +1,12 @@ -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'; @@ -16,7 +18,6 @@ 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 crypto from 'crypto'; @@ -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; @@ -63,9 +59,7 @@ export default class ChargingStation { this._hasStopped = false; this._hasSocketRestarted = false; - this._connectionTimeout = Configuration.getConnectionTimeout() * 1000; // Ms, zero for disabling this._autoReconnectRetryCount = 0; - this._autoReconnectMaxRetries = Configuration.getAutoReconnectMaxRetries(); // -1 for unlimited this._requests = {} as Requests; this._messageQueue = [] as string[]; @@ -106,7 +100,7 @@ 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 }, @@ -115,6 +109,8 @@ export default class ChargingStation { 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) { @@ -122,7 +118,10 @@ export default class ChargingStation { } const templateMaxConnectors = this._getTemplateMaxNumberOfConnectors(); if (templateMaxConnectors <= 0) { - logger.warn(`${this._logPrefix()} Charging station template ${this._stationTemplateFile} with no connector configurations`); + logger.warn(`${this._logPrefix()} Charging station template ${this._stationTemplateFile} with no connector configuration`); + } + if (!this._stationInfo.Connectors[0]) { + logger.warn(`${this._logPrefix()} Charging station template ${this._stationTemplateFile} with no connector Id 0 configuration`); } // Sanity check if (maxConnectors > (this._stationInfo.Connectors[0] ? templateMaxConnectors - 1 : templateMaxConnectors) && !this._stationInfo.randomConnectors) { @@ -136,7 +135,7 @@ export default class ChargingStation { // Add connector Id 0 let lastConnector = '0'; for (lastConnector in this._stationInfo.Connectors) { - if (Utils.convertToInt(lastConnector) === 0 && this._stationInfo.useConnectorId0 && this._stationInfo.Connectors[lastConnector]) { + if (Utils.convertToInt(lastConnector) === 0 && this._getUseConnectorId0() && this._stationInfo.Connectors[lastConnector]) { this._connectors[lastConnector] = Utils.cloneObject(this._stationInfo.Connectors[lastConnector]) as Connector; } } @@ -152,7 +151,7 @@ export default class ChargingStation { delete this._stationInfo.Connectors; // Initialize transaction attributes on connectors for (const connector in this._connectors) { - if (!this.getConnector(Utils.convertToInt(connector)).transactionStarted) { + if (Utils.convertToInt(connector) > 0 && !this.getConnector(Utils.convertToInt(connector)).transactionStarted) { this._initTransactionOnConnector(Utils.convertToInt(connector)); } } @@ -193,6 +192,10 @@ export default class ChargingStation { return this._stationInfo.authorizationFile && this._stationInfo.authorizationFile; } + _getUseConnectorId0(): boolean { + return !Utils.isUndefined(this._stationInfo.useConnectorId0) ? this._stationInfo.useConnectorId0 : true; + } + _loadAndGetAuthorizedTags(): string[] { let authorizedTags: string[] = []; const authorizationFile = this._getAuthorizationFile(); @@ -228,7 +231,7 @@ export default class ChargingStation { _getNumberOfPhases(): number { switch (this._getPowerOutType()) { case PowerOutType.AC: - return !Utils.isUndefined(this._stationInfo.numberOfPhases) ? Utils.convertToInt(this._stationInfo.numberOfPhases) : 3; + return !Utils.isUndefined(this._stationInfo.numberOfPhases) ? this._stationInfo.numberOfPhases : 3; case PowerOutType.DC: return 0; } @@ -237,13 +240,33 @@ export default class ChargingStation { _getNumberOfRunningTransactions(): number { let trxCount = 0; for (const connector in this._connectors) { - if (this.getConnector(Utils.convertToInt(connector)).transactionStarted) { + if (Utils.convertToInt(connector) > 0 && this.getConnector(Utils.convertToInt(connector)).transactionStarted) { trxCount++; } } 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) { @@ -292,17 +315,25 @@ export default class ChargingStation { logger.error(errMsg); throw Error(errMsg); } - return !Utils.isUndefined(this._stationInfo.voltageOut) ? Utils.convertToInt(this._stationInfo.voltageOut) : defaultVoltageOut; + return !Utils.isUndefined(this._stationInfo.voltageOut) ? this._stationInfo.voltageOut : defaultVoltageOut; } _getTransactionIdTag(transactionId: number): string { for (const connector in this._connectors) { - if (this.getConnector(Utils.convertToInt(connector)).transactionId === transactionId) { + if (Utils.convertToInt(connector) > 0 && this.getConnector(Utils.convertToInt(connector)).transactionId === transactionId) { return this.getConnector(Utils.convertToInt(connector)).idTag; } } } + _getTransactionMeterStop(transactionId: number): number { + for (const connector in this._connectors) { + if (Utils.convertToInt(connector) > 0 && 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; } @@ -343,7 +374,9 @@ export default class ChargingStation { this._startHeartbeat(); // Initialize connectors status for (const connector in this._connectors) { - if (!this._hasStopped && !this.getConnector(Utils.convertToInt(connector)).status && this.getConnector(Utils.convertToInt(connector)).bootStatus) { + if (Utils.convertToInt(connector) === 0) { + continue; + } else 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) { @@ -383,7 +416,7 @@ export default class ChargingStation { await this._automaticTransactionGeneration.stop(reason); } else { for (const connector in this._connectors) { - if (this.getConnector(Utils.convertToInt(connector)).transactionStarted) { + if (Utils.convertToInt(connector) > 0 && this.getConnector(Utils.convertToInt(connector)).transactionStarted) { await this.sendStopTransaction(this.getConnector(Utils.convertToInt(connector)).transactionId, reason); } } @@ -395,7 +428,7 @@ export default class ChargingStation { if (webSocketPingInterval > 0 && !this._webSocketPingSetInterval) { this._webSocketPingSetInterval = setInterval(() => { if (this._wsConnection?.readyState === WebSocket.OPEN) { - this._wsConnection.ping((): void => {}); + this._wsConnection.ping((): void => { }); } }, webSocketPingInterval * 1000); logger.info(this._logPrefix() + ' WebSocket ping started every ' + Utils.secondsToHHMMSS(webSocketPingInterval)); @@ -503,13 +536,16 @@ export default class ChargingStation { } } - _openWSConnection(options?: WebSocket.ClientOptions): void { + _openWSConnection(options?: WebSocket.ClientOptions, forceCloseOpened = false): void { if (Utils.isUndefined(options)) { options = {} as WebSocket.ClientOptions; } if (Utils.isUndefined(options.handshakeTimeout)) { options.handshakeTimeout = this._connectionTimeout; } + if (this._wsConnection?.readyState === WebSocket.OPEN && forceCloseOpened) { + this._wsConnection.close(); + } this._wsConnection = new WebSocket(this._wsConnectionUrl, 'ocpp' + Constants.OCPP_VERSION_16, options); logger.info(this._logPrefix() + ' Will communicate through URL ' + this._supervisionUrl); } @@ -537,13 +573,15 @@ export default class ChargingStation { async stop(reason: StopTransactionReason = StopTransactionReason.NONE): Promise { // 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 (Utils.convertToInt(connector) > 0) { + await this.sendStatusNotification(Utils.convertToInt(connector), ChargePointStatus.UNAVAILABLE); + } } if (this._wsConnection?.readyState === WebSocket.OPEN) { this._wsConnection.close(); } + this._bootNotificationResponse = null; this._hasStopped = true; } @@ -564,7 +602,8 @@ export default class ChargingStation { 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 }); + this._openWSConnection({ handshakeTimeout: reconnectDelay - 100 }); + this._hasSocketRestarted = true; } else if (this._autoReconnectMaxRetries !== -1) { logger.error(`${this._logPrefix()} Socket: max retries reached (${this._autoReconnectRetryCount}) or retry disabled (${this._autoReconnectMaxRetries})`); } @@ -574,10 +613,18 @@ export default class ChargingStation { logger.info(`${this._logPrefix()} Is connected to server through ${this._wsConnectionUrl}`); if (!this._hasSocketRestarted || this._hasStopped) { // Send BootNotification - await this.sendBootNotification(); + this._bootNotificationResponse = await this.sendBootNotification(); } - await this._startMessageSequence(); - if (this._hasSocketRestarted) { + 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._bootNotificationResponse.status === RegistrationStatus.ACCEPTED) { if (!Utils.isEmptyArray(this._messageQueue)) { this._messageQueue.forEach((message, index) => { if (this._wsConnection?.readyState === WebSocket.OPEN) { @@ -594,7 +641,6 @@ export default class ChargingStation { async onError(errorEvent): Promise { switch (errorEvent.code) { case 'ECONNREFUSED': - this._hasSocketRestarted = true; await this._reconnect(errorEvent); break; default: @@ -611,7 +657,6 @@ export default class ChargingStation { this._autoReconnectRetryCount = 0; break; default: // Abnormal close - this._hasSocketRestarted = true; await this._reconnect(closeEvent); break; } @@ -691,9 +736,7 @@ export default class ChargingStation { 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); @@ -701,9 +744,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; @@ -713,7 +756,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, @@ -727,7 +770,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, @@ -743,10 +786,10 @@ export default class ChargingStation { async sendStopTransaction(transactionId: number, reason: StopTransactionReason = StopTransactionReason.NONE): Promise { 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 }, }; @@ -760,33 +803,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, @@ -794,14 +834,13 @@ 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); let phaseValue: string; - if (voltageValue >= 0 && voltageValue <= 250) { + if (self._getVoltageOut() >= 0 && self._getVoltageOut() <= 250) { phaseValue = `L${phase}-N`; - } else if (voltageValue > 250) { + } else if (self._getVoltageOut() > 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, @@ -848,20 +887,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 }, @@ -909,20 +948,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 }, @@ -952,7 +991,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 }, @@ -960,21 +999,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) { @@ -994,7 +1032,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) { @@ -1049,12 +1087,12 @@ export default class ChargingStation { } // Function that will receive the request's response - function responseCallback(payload, requestPayload): void { + async function responseCallback(payload, requestPayload): Promise { if (self.getEnableStatistics()) { self._statistics.addMessage(commandName, messageType); } // Send the response - self.handleResponse(commandName, payload, requestPayload); + await self.handleResponse(commandName, payload, requestPayload); resolve(payload); } @@ -1073,23 +1111,23 @@ export default class ChargingStation { }); } - handleResponse(commandName: string, payload, requestPayload): void { + async handleResponse(commandName: string, payload, requestPayload): Promise { const responseCallbackFn = 'handleResponse' + commandName; if (typeof this[responseCallbackFn] === 'function') { - this[responseCallbackFn](payload, requestPayload); + await this[responseCallbackFn](payload, requestPayload); } else { logger.error(this._logPrefix() + ' Trying to call an undefined response callback function: ' + responseCallbackFn); } } - handleResponseBootNotification(payload, requestPayload): void { - if (payload.status === 'Accepted') { - this._heartbeatInterval = Utils.convertToInt(payload.interval) * 1000; + handleResponseBootNotification(payload: BootNotificationResponse, requestPayload: BootNotificationRequest): void { + if (payload.status === RegistrationStatus.ACCEPTED) { + this._heartbeatInterval = payload.interval * 1000; this._heartbeatSetInterval ? this._restartHeartbeat() : this._startHeartbeat(); - this._addConfigurationKey('HeartBeatInterval', payload.interval); - this._addConfigurationKey('HeartbeatInterval', payload.interval, false, false); + 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'); @@ -1110,8 +1148,8 @@ export default class ChargingStation { } } - handleResponseStartTransaction(payload: StartTransactionResponse, requestPayload): void { - const connectorId = Utils.convertToInt(requestPayload.connectorId); + async handleResponseStartTransaction(payload: StartTransactionResponse, requestPayload: StartTransactionRequest): Promise { + const connectorId = requestPayload.connectorId; if (this.getConnector(connectorId).transactionStarted) { logger.debug(this._logPrefix() + ' Trying to start a transaction on an already used connector ' + connectorId.toString() + ': %j', this.getConnector(connectorId)); return; @@ -1119,7 +1157,7 @@ export default class ChargingStation { let transactionConnectorId: number; for (const connector in this._connectors) { - if (Utils.convertToInt(connector) === connectorId) { + if (Utils.convertToInt(connector) > 0 && Utils.convertToInt(connector) === connectorId) { transactionConnectorId = Utils.convertToInt(connector); break; } @@ -1133,7 +1171,7 @@ export default class ChargingStation { this.getConnector(connectorId).transactionId = payload.transactionId; this.getConnector(connectorId).idTag = requestPayload.idTag; this.getConnector(connectorId).lastEnergyActiveImportRegisterValue = 0; - this.sendStatusNotification(connectorId, ChargePointStatus.CHARGING).catch(() => { }); + await this.sendStatusNotification(connectorId, ChargePointStatus.CHARGING); logger.info(this._logPrefix() + ' Transaction ' + payload.transactionId.toString() + ' STARTED on ' + this._stationInfo.name + '#' + connectorId.toString() + ' for idTag ' + requestPayload.idTag); if (this._stationInfo.powerSharedByConnectors) { this._stationInfo.powerDivider++; @@ -1142,45 +1180,45 @@ 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(() => { }); + await this.sendStatusNotification(connectorId, ChargePointStatus.AVAILABLE); } } - handleResponseStopTransaction(payload: StopTransactionResponse, requestPayload): void { + async handleResponseStopTransaction(payload: StopTransactionResponse, requestPayload: StopTransactionRequest): Promise { let transactionConnectorId: number; for (const connector in this._connectors) { - if (this.getConnector(Utils.convertToInt(connector)).transactionId === Utils.convertToInt(requestPayload.transactionId)) { + if (Utils.convertToInt(connector) > 0 && this.getConnector(Utils.convertToInt(connector)).transactionId === requestPayload.transactionId) { transactionConnectorId = Utils.convertToInt(connector); break; } } if (!transactionConnectorId) { - logger.error(this._logPrefix() + ' Trying 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) { - this.sendStatusNotification(transactionConnectorId, ChargePointStatus.AVAILABLE).catch(() => { }); + await this.sendStatusNotification(transactionConnectorId, ChargePointStatus.AVAILABLE); 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); } @@ -1208,7 +1246,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); @@ -1218,8 +1256,12 @@ export default class ChargingStation { return Constants.OCPP_RESPONSE_ACCEPTED; } - async handleRequestUnlockConnector(commandPayload): Promise { - const connectorId = Utils.convertToInt(commandPayload.connectorId); + handleRequestClearCache(): DefaultResponse { + return Constants.OCPP_RESPONSE_ACCEPTED; + } + + async handleRequestUnlockConnector(commandPayload: UnlockConnectorRequest): Promise { + const connectorId = commandPayload.connectorId; if (connectorId === 0) { logger.error(this._logPrefix() + ' Trying to unlock connector ' + connectorId.toString()); return Constants.OCPP_RESPONSE_UNLOCK_NOT_SUPPORTED; @@ -1260,7 +1302,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)) { @@ -1278,7 +1320,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)) { @@ -1303,7 +1345,14 @@ export default class ChargingStation { }; } - handleRequestChangeConfiguration(commandPayload): ConfigurationResponse { + handleRequestChangeConfiguration(commandPayload: ChangeConfigurationRequest): ChangeConfigurationResponse { + // JSON request fields type sanity check + if (!Utils.isString(commandPayload.key)) { + logger.error(`${this._logPrefix()} ChangeConfiguration request key field is not a string:`, commandPayload); + } + if (!Utils.isString(commandPayload.value)) { + logger.error(`${this._logPrefix()} ChangeConfiguration request value field is not a string:`, commandPayload); + } const keyToChange = this._getConfigurationKey(commandPayload.key); if (!keyToChange) { return Constants.OCPP_CONFIGURATION_RESPONSE_NOT_SUPPORTED; @@ -1313,7 +1362,7 @@ export default class ChargingStation { const keyIndex = this._configuration.configurationKey.indexOf(keyToChange); let valueChanged = false; if (this._configuration.configurationKey[keyIndex].value !== commandPayload.value) { - this._configuration.configurationKey[keyIndex].value = commandPayload.value as string; + this._configuration.configurationKey[keyIndex].value = commandPayload.value; valueChanged = true; } let triggerHeartbeatRestart = false; @@ -1339,8 +1388,27 @@ export default class ChargingStation { } } - async handleRequestRemoteStartTransaction(commandPayload): Promise { - const transactionConnectorID: number = commandPayload.connectorId ? Utils.convertToInt(commandPayload.connectorId) : 1; + 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 ? commandPayload.connectorId : 1; if (this._getAuthorizeRemoteTxRequests() && this._getLocalAuthListEnabled() && this.hasAuthorizedTags()) { // Check if authorized if (this._authorizedTags.find((value) => value === commandPayload.idTag)) { @@ -1349,7 +1417,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 @@ -1358,10 +1426,10 @@ export default class ChargingStation { return Constants.OCPP_RESPONSE_ACCEPTED; } - async handleRequestRemoteStopTransaction(commandPayload): Promise { - const transactionId = Utils.convertToInt(commandPayload.transactionId); + async handleRequestRemoteStopTransaction(commandPayload: RemoteStopTransactionRequest): Promise { + const transactionId = commandPayload.transactionId; for (const connector in this._connectors) { - if (this.getConnector(Utils.convertToInt(connector)).transactionId === transactionId) { + if (Utils.convertToInt(connector) > 0 && this.getConnector(Utils.convertToInt(connector)).transactionId === transactionId) { await this.sendStopTransaction(transactionId); return Constants.OCPP_RESPONSE_ACCEPTED; }