X-Git-Url: https://git.piment-noir.org/?a=blobdiff_plain;f=src%2Fcharging-station%2FChargingStation.ts;h=667c154b35595feb376ef14dbe175bfca3aa61cd;hb=8c476a1f1117b85fa29a435fc8733a482472139f;hp=85045f7eb0aec2b391f9f6d041cdfe6ee95b8174;hpb=136c90bad539d32137cfabd0d2d97bf0aa89be66;p=e-mobility-charging-stations-simulator.git diff --git a/src/charging-station/ChargingStation.ts b/src/charging-station/ChargingStation.ts index 85045f7e..667c154b 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; @@ -42,9 +38,9 @@ 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; @@ -64,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[]; @@ -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) { @@ -244,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) { @@ -295,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; @@ -303,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; } @@ -322,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; @@ -391,7 +419,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)); @@ -499,8 +527,14 @@ export default class ChargingStation { } } - _openWSConnection(): void { - this._wsConnection = new WebSocket(this._wsConnectionUrl, 'ocpp' + Constants.OCPP_VERSION_16); + _openWSConnection(options?: WebSocket.ClientOptions): void { + if (Utils.isUndefined(options)) { + options = {} as WebSocket.ClientOptions; + } + 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); } @@ -534,10 +568,11 @@ export default class ChargingStation { if (this._wsConnection?.readyState === WebSocket.OPEN) { this._wsConnection.close(); } + this._bootNotificationResponse = null; this._hasStopped = true; } - _reconnect(error): void { + async _reconnect(error): Promise { logger.error(this._logPrefix() + ' Socket: abnormally closed: %j', error); // Stop heartbeat this._stopHeartbeat(); @@ -548,16 +583,15 @@ export default class ChargingStation { !this._automaticTransactionGeneration.timeToStop) { this._automaticTransactionGeneration.stop().catch(() => { }); } - 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._openWSConnection(); - }, 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})`); } } @@ -565,10 +599,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) { @@ -578,15 +620,15 @@ export default class ChargingStation { }); } } - this._hasSocketRestarted = false; this._autoReconnectRetryCount = 0; + this._hasSocketRestarted = false; } - onError(errorEvent): void { + 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); @@ -594,7 +636,7 @@ export default class ChargingStation { } } - onClose(closeEvent): void { + async onClose(closeEvent): Promise { switch (closeEvent) { case 1000: // Normal close case 1005: @@ -603,7 +645,7 @@ export default class ChargingStation { break; default: // Abnormal close this._hasSocketRestarted = true; - this._reconnect(closeEvent); + await this._reconnect(closeEvent); break; } } @@ -682,9 +724,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); @@ -692,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; @@ -704,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, @@ -718,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, @@ -732,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 }, }; @@ -751,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, @@ -785,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, @@ -839,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 }, @@ -900,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 }, @@ -943,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 }, @@ -951,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) { @@ -985,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) { @@ -1073,14 +1109,14 @@ export default class ChargingStation { } } - handleResponseBootNotification(payload, requestPayload): void { - if (payload.status === 'Accepted') { + 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); - 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'); @@ -1101,7 +1137,7 @@ 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() + ' Trying to start a transaction on an already used connector ' + connectorId.toString() + ': %j', this.getConnector(connectorId)); @@ -1133,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)) { @@ -1148,7 +1184,7 @@ export default class ChargingStation { } } 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) { @@ -1156,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); } @@ -1199,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); @@ -1209,7 +1245,11 @@ 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() + ' Trying to unlock connector ' + connectorId.toString()); @@ -1251,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)) { @@ -1269,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)) { @@ -1294,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; @@ -1304,7 +1344,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; @@ -1330,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 @@ -1340,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 @@ -1349,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) {