Add exponential delay at reconnect
[e-mobility-charging-stations-simulator.git] / src / charging-station / ChargingStation.ts
CommitLineData
29bf6658 1import { AuthorizationStatus, StartTransactionResponse, StopTransactionReason, StopTransactionResponse } from '../types/ocpp/1.6/Transaction';
e118beaa 2import ChargingStationConfiguration, { ConfigurationKey } from '../types/ChargingStationConfiguration';
9ac86a7e 3import ChargingStationTemplate, { PowerOutType } from '../types/ChargingStationTemplate';
29bf6658 4import { ConfigurationResponse, DefaultRequestResponse, UnlockResponse } from '../types/ocpp/1.6/RequestResponses';
10570d97 5import Connectors, { Connector } from '../types/Connectors';
29bf6658 6import MeterValue, { MeterValueLocation, MeterValueMeasurand, MeterValuePhase, MeterValueUnit } from '../types/ocpp/1.6/MeterValue';
6af9012e 7import { PerformanceObserver, performance } from 'perf_hooks';
136c90ba 8import WebSocket, { MessageEvent } from 'ws';
3f40bc9c 9
6af9012e 10import AutomaticTransactionGenerator from './AutomaticTransactionGenerator';
29bf6658
JB
11import { ChargePointErrorCode } from '../types/ocpp/1.6/ChargePointErrorCode';
12import { ChargePointStatus } from '../types/ocpp/1.6/ChargePointStatus';
9ac86a7e 13import ChargingStationInfo from '../types/ChargingStationInfo';
6af9012e 14import Configuration from '../utils/Configuration';
63b48f77 15import Constants from '../utils/Constants';
6af9012e 16import ElectricUtils from '../utils/ElectricUtils';
6b0ce541 17import MeasurandValues from '../types/MeasurandValues';
63b48f77
JB
18import OCPPError from './OcppError';
19import Requests from '../types/ocpp/1.6/Requests';
6af9012e
JB
20import Statistics from '../utils/Statistics';
21import Utils from '../utils/Utils';
3f40bc9c
JB
22import crypto from 'crypto';
23import fs from 'fs';
6af9012e 24import logger from '../utils/Logger';
3f40bc9c
JB
25
26export default class ChargingStation {
6af9012e 27 private _index: number;
a4a21709 28 private _stationTemplateFile: string;
9ac86a7e 29 private _stationInfo: ChargingStationInfo;
e118beaa
JB
30 private _bootNotificationMessage: {
31 chargePointModel: string,
32 chargePointVendor: string,
33 chargeBoxSerialNumber?: string,
34 firmwareVersion?: string,
35 };
36
10570d97 37 private _connectors: Connectors;
e118beaa 38 private _configuration: ChargingStationConfiguration;
a4a21709 39 private _connectorsConfigurationHash: string;
10570d97
JB
40 private _supervisionUrl: string;
41 private _wsConnectionUrl: string;
a4a21709 42 private _wsConnection: WebSocket;
9ac86a7e
JB
43 private _hasStopped: boolean;
44 private _hasSocketRestarted: boolean;
032d6efc 45 private _connectionTimeout: number;
a4a21709
JB
46 private _autoReconnectRetryCount: number;
47 private _autoReconnectMaxRetries: number;
63b48f77
JB
48 private _requests: Requests;
49 private _messageQueue: string[];
6af9012e
JB
50 private _automaticTransactionGeneration: AutomaticTransactionGenerator;
51 private _authorizedTags: string[];
a4a21709 52 private _heartbeatInterval: number;
10570d97 53 private _heartbeatSetInterval: NodeJS.Timeout;
136c90ba 54 private _webSocketPingSetInterval: NodeJS.Timeout;
6af9012e
JB
55 private _statistics: Statistics;
56 private _performanceObserver: PerformanceObserver;
57
58 constructor(index: number, stationTemplateFile: string) {
2e6f5966
JB
59 this._index = index;
60 this._stationTemplateFile = stationTemplateFile;
63b48f77 61 this._connectors = {} as Connectors;
2e6f5966
JB
62 this._initialize();
63
9ac86a7e
JB
64 this._hasStopped = false;
65 this._hasSocketRestarted = false;
032d6efc 66 this._connectionTimeout = Configuration.getConnectionTimeout() * 1000; // Ms, zero for disabling
7dde0b73
JB
67 this._autoReconnectRetryCount = 0;
68 this._autoReconnectMaxRetries = Configuration.getAutoReconnectMaxRetries(); // -1 for unlimited
2e6f5966 69
63b48f77
JB
70 this._requests = {} as Requests;
71 this._messageQueue = [] as string[];
2e6f5966 72
83045896 73 this._authorizedTags = this._loadAndGetAuthorizedTags();
2e6f5966
JB
74 }
75
9ac86a7e
JB
76 _getStationName(stationTemplate: ChargingStationTemplate): string {
77 return stationTemplate.fixedName ? stationTemplate.baseName : stationTemplate.baseName + '-' + ('000000000' + this._index.toString()).substr(('000000000' + this._index.toString()).length - 4);
5ad8570f
JB
78 }
79
9ac86a7e
JB
80 _buildStationInfo(): ChargingStationInfo {
81 let stationTemplateFromFile: ChargingStationTemplate;
5ad8570f
JB
82 try {
83 // Load template file
84 const fileDescriptor = fs.openSync(this._stationTemplateFile, 'r');
9ac86a7e 85 stationTemplateFromFile = JSON.parse(fs.readFileSync(fileDescriptor, 'utf8')) as ChargingStationTemplate;
5ad8570f
JB
86 fs.closeSync(fileDescriptor);
87 } catch (error) {
7ec46a9a 88 logger.error('Template file ' + this._stationTemplateFile + ' loading error: %j', error);
cdd9fed5 89 throw error;
5ad8570f 90 }
9ac86a7e 91 const stationInfo: ChargingStationInfo = stationTemplateFromFile || {} as ChargingStationInfo;
0a60c33c 92 if (!Utils.isEmptyArray(stationTemplateFromFile.power)) {
9ac86a7e
JB
93 stationTemplateFromFile.power = stationTemplateFromFile.power as number[];
94 stationInfo.maxPower = stationTemplateFromFile.power[Math.floor(Math.random() * stationTemplateFromFile.power.length)];
5ad8570f 95 } else {
9ac86a7e 96 stationInfo.maxPower = stationTemplateFromFile.power as number;
5ad8570f 97 }
9ac86a7e
JB
98 stationInfo.name = this._getStationName(stationTemplateFromFile);
99 stationInfo.resetTime = stationTemplateFromFile.resetTime ? stationTemplateFromFile.resetTime * 1000 : Constants.CHARGING_STATION_DEFAULT_RESET_TIME;
100 return stationInfo;
5ad8570f
JB
101 }
102
9ac86a7e 103 get stationInfo(): ChargingStationInfo {
6af9012e
JB
104 return this._stationInfo;
105 }
106
107 _initialize(): void {
2e6f5966
JB
108 this._stationInfo = this._buildStationInfo();
109 this._bootNotificationMessage = {
110 chargePointModel: this._stationInfo.chargePointModel,
111 chargePointVendor: this._stationInfo.chargePointVendor,
6af9012e
JB
112 ...!Utils.isUndefined(this._stationInfo.chargeBoxSerialNumberPrefix) && { chargeBoxSerialNumber: this._stationInfo.chargeBoxSerialNumberPrefix },
113 ...!Utils.isUndefined(this._stationInfo.firmwareVersion) && { firmwareVersion: this._stationInfo.firmwareVersion },
2e6f5966 114 };
136c90ba 115 this._configuration = this._getTemplateChargingStationConfiguration();
2e6f5966 116 this._supervisionUrl = this._getSupervisionURL();
0a60c33c
JB
117 this._wsConnectionUrl = this._supervisionUrl + '/' + this._stationInfo.name;
118 // Build connectors if needed
6ecb15e4
JB
119 const maxConnectors = this._getMaxNumberOfConnectors();
120 if (maxConnectors <= 0) {
3f40bc9c 121 logger.warn(`${this._logPrefix()} Charging station template ${this._stationTemplateFile} with ${maxConnectors} connectors`);
7abfea5f
JB
122 }
123 const templateMaxConnectors = this._getTemplateMaxNumberOfConnectors();
124 if (templateMaxConnectors <= 0) {
3f40bc9c 125 logger.warn(`${this._logPrefix()} Charging station template ${this._stationTemplateFile} with no connector configurations`);
7abfea5f
JB
126 }
127 // Sanity check
9ac86a7e 128 if (maxConnectors > (this._stationInfo.Connectors[0] ? templateMaxConnectors - 1 : templateMaxConnectors) && !this._stationInfo.randomConnectors) {
7abfea5f
JB
129 logger.warn(`${this._logPrefix()} Number of connectors exceeds the number of connector configurations in template ${this._stationTemplateFile}, forcing random connector configurations affectation`);
130 this._stationInfo.randomConnectors = true;
6ecb15e4 131 }
8bce55bf 132 const connectorsConfigHash = crypto.createHash('sha256').update(JSON.stringify(this._stationInfo.Connectors) + maxConnectors.toString()).digest('hex');
de1f5008 133 // FIXME: Handle shrinking the number of connectors
5a9f5716 134 if (!this._connectors || (this._connectors && this._connectorsConfigurationHash !== connectorsConfigHash)) {
de1f5008 135 this._connectorsConfigurationHash = connectorsConfigHash;
7abfea5f 136 // Add connector Id 0
6af9012e 137 let lastConnector = '0';
8bce55bf 138 for (lastConnector in this._stationInfo.Connectors) {
9ac86a7e
JB
139 if (Utils.convertToInt(lastConnector) === 0 && this._stationInfo.useConnectorId0 && this._stationInfo.Connectors[lastConnector]) {
140 this._connectors[lastConnector] = Utils.cloneObject(this._stationInfo.Connectors[lastConnector]) as Connector;
0a60c33c
JB
141 }
142 }
0a60c33c 143 // Generate all connectors
7abfea5f
JB
144 if ((this._stationInfo.Connectors[0] ? templateMaxConnectors - 1 : templateMaxConnectors) > 0) {
145 for (let index = 1; index <= maxConnectors; index++) {
9ac86a7e
JB
146 const randConnectorID = this._stationInfo.randomConnectors ? Utils.getRandomInt(Utils.convertToInt(lastConnector), 1) : index;
147 this._connectors[index] = Utils.cloneObject(this._stationInfo.Connectors[randConnectorID]) as Connector;
7abfea5f 148 }
0a60c33c
JB
149 }
150 }
d4a73fb7
JB
151 // Avoid duplication of connectors related information
152 delete this._stationInfo.Connectors;
0a60c33c
JB
153 // Initialize transaction attributes on connectors
154 for (const connector in this._connectors) {
6af9012e 155 if (!this.getConnector(Utils.convertToInt(connector)).transactionStarted) {
10570d97 156 this._initTransactionOnConnector(Utils.convertToInt(connector));
0a60c33c
JB
157 }
158 }
7abfea5f 159 // OCPP parameters
10570d97 160 this._addConfigurationKey('NumberOfConnectors', this._getNumberOfConnectors().toString(), true);
7abfea5f 161 if (!this._getConfigurationKey('MeterValuesSampledData')) {
10570d97 162 this._addConfigurationKey('MeterValuesSampledData', MeterValueMeasurand.ENERGY_ACTIVE_IMPORT_REGISTER);
7abfea5f 163 }
6ecb15e4 164 this._stationInfo.powerDivider = this._getPowerDivider();
8bce55bf
JB
165 if (this.getEnableStatistics()) {
166 this._statistics = Statistics.getInstance();
167 this._statistics.objName = this._stationInfo.name;
168 this._performanceObserver = new PerformanceObserver((list) => {
169 const entry = list.getEntries()[0];
10570d97 170 this._statistics.logPerformance(entry, Constants.ENTITY_CHARGING_STATION);
8bce55bf
JB
171 this._performanceObserver.disconnect();
172 });
173 }
7dde0b73
JB
174 }
175
10570d97 176 get connectors(): Connectors {
6af9012e
JB
177 return this._connectors;
178 }
179
180 get statistics(): Statistics {
181 return this._statistics;
182 }
183
184 _logPrefix(): string {
ead548f2 185 return Utils.logPrefix(` ${this._stationInfo.name}:`);
7dde0b73
JB
186 }
187
136c90ba 188 _getTemplateChargingStationConfiguration(): ChargingStationConfiguration {
e118beaa 189 return this._stationInfo.Configuration ? this._stationInfo.Configuration : {} as ChargingStationConfiguration;
7dde0b73
JB
190 }
191
10570d97 192 _getAuthorizationFile(): string {
6af9012e 193 return this._stationInfo.authorizationFile && this._stationInfo.authorizationFile;
7dde0b73
JB
194 }
195
6af9012e 196 _loadAndGetAuthorizedTags(): string[] {
65c5527e 197 let authorizedTags: string[] = [];
2e6f5966
JB
198 const authorizationFile = this._getAuthorizationFile();
199 if (authorizationFile) {
200 try {
201 // Load authorization file
202 const fileDescriptor = fs.openSync(authorizationFile, 'r');
10570d97 203 authorizedTags = JSON.parse(fs.readFileSync(fileDescriptor, 'utf8')) as string[];
2e6f5966
JB
204 fs.closeSync(fileDescriptor);
205 } catch (error) {
7ec46a9a 206 logger.error(this._logPrefix() + ' Authorization file ' + authorizationFile + ' loading error: %j', error);
cdd9fed5 207 throw error;
2e6f5966
JB
208 }
209 } else {
ead548f2 210 logger.info(this._logPrefix() + ' No authorization file given in template file ' + this._stationTemplateFile);
2e6f5966
JB
211 }
212 return authorizedTags;
213 }
214
65c5527e 215 getRandomTagId(): string {
5ad8570f
JB
216 const index = Math.floor(Math.random() * this._authorizedTags.length);
217 return this._authorizedTags[index];
2e6f5966
JB
218 }
219
65c5527e 220 hasAuthorizedTags(): boolean {
5ad8570f
JB
221 return !Utils.isEmptyArray(this._authorizedTags);
222 }
223
65c5527e 224 getEnableStatistics(): boolean {
9ac86a7e 225 return !Utils.isUndefined(this._stationInfo.enableStatistics) ? this._stationInfo.enableStatistics : true;
2328be1e
JB
226 }
227
6af9012e 228 _getNumberOfPhases(): number {
8c4da341 229 switch (this._getPowerOutType()) {
9ac86a7e 230 case PowerOutType.AC:
8c4da341 231 return !Utils.isUndefined(this._stationInfo.numberOfPhases) ? Utils.convertToInt(this._stationInfo.numberOfPhases) : 3;
9ac86a7e 232 case PowerOutType.DC:
8c4da341
JB
233 return 0;
234 }
8bce55bf
JB
235 }
236
65c5527e 237 _getNumberOfRunningTransactions(): number {
6ecb15e4
JB
238 let trxCount = 0;
239 for (const connector in this._connectors) {
6af9012e 240 if (this.getConnector(Utils.convertToInt(connector)).transactionStarted) {
6ecb15e4
JB
241 trxCount++;
242 }
243 }
244 return trxCount;
245 }
246
65c5527e 247 _getPowerDivider(): number {
7abfea5f 248 let powerDivider = this._getNumberOfConnectors();
6ecb15e4
JB
249 if (this._stationInfo.powerSharedByConnectors) {
250 powerDivider = this._getNumberOfRunningTransactions();
251 }
252 return powerDivider;
253 }
254
10570d97 255 getConnector(id: number): Connector {
6af9012e 256 return this._connectors[id];
6ecb15e4
JB
257 }
258
65c5527e 259 _getTemplateMaxNumberOfConnectors(): number {
7abfea5f
JB
260 return Object.keys(this._stationInfo.Connectors).length;
261 }
262
65c5527e 263 _getMaxNumberOfConnectors(): number {
5ad8570f 264 let maxConnectors = 0;
0a60c33c 265 if (!Utils.isEmptyArray(this._stationInfo.numberOfConnectors)) {
9ac86a7e 266 const numberOfConnectors = this._stationInfo.numberOfConnectors as number[];
6ecb15e4 267 // Distribute evenly the number of connectors
7ec46a9a 268 maxConnectors = numberOfConnectors[(this._index - 1) % numberOfConnectors.length];
7abfea5f 269 } else if (!Utils.isUndefined(this._stationInfo.numberOfConnectors)) {
9ac86a7e 270 maxConnectors = this._stationInfo.numberOfConnectors as number;
488fd3a7 271 } else {
7abfea5f 272 maxConnectors = this._stationInfo.Connectors[0] ? this._getTemplateMaxNumberOfConnectors() - 1 : this._getTemplateMaxNumberOfConnectors();
5ad8570f
JB
273 }
274 return maxConnectors;
2e6f5966
JB
275 }
276
6af9012e 277 _getNumberOfConnectors(): number {
7abfea5f 278 return this._connectors[0] ? Object.keys(this._connectors).length - 1 : Object.keys(this._connectors).length;
6ecb15e4
JB
279 }
280
65c5527e 281 _getVoltageOut(): number {
b2acff85 282 const errMsg = `${this._logPrefix()} Unknown ${this._getPowerOutType()} powerOutType in template file ${this._stationTemplateFile}, cannot define default voltage out`;
10570d97 283 let defaultVoltageOut: number;
b2acff85 284 switch (this._getPowerOutType()) {
9ac86a7e 285 case PowerOutType.AC:
b2acff85
JB
286 defaultVoltageOut = 230;
287 break;
9ac86a7e 288 case PowerOutType.DC:
b2acff85
JB
289 defaultVoltageOut = 400;
290 break;
291 default:
292 logger.error(errMsg);
293 throw Error(errMsg);
294 }
295 return !Utils.isUndefined(this._stationInfo.voltageOut) ? Utils.convertToInt(this._stationInfo.voltageOut) : defaultVoltageOut;
296 }
297
032d6efc 298 _getTransactionIdTag(transactionId: number): string {
9ac86a7e
JB
299 for (const connector in this._connectors) {
300 if (this.getConnector(Utils.convertToInt(connector)).transactionId === transactionId) {
301 return this.getConnector(Utils.convertToInt(connector)).idTag;
302 }
303 }
304 }
305
306 _getPowerOutType(): PowerOutType {
307 return !Utils.isUndefined(this._stationInfo.powerOutType) ? this._stationInfo.powerOutType : PowerOutType.AC;
3f40bc9c
JB
308 }
309
65c5527e 310 _getSupervisionURL(): string {
7ec46a9a 311 const supervisionUrls = Utils.cloneObject(this._stationInfo.supervisionURL ? this._stationInfo.supervisionURL : Configuration.getSupervisionURLs()) as string | string[];
7dde0b73 312 let indexUrl = 0;
0a60c33c 313 if (!Utils.isEmptyArray(supervisionUrls)) {
524d9cb3 314 if (Configuration.getDistributeStationsToTenantsEqually()) {
2e6f5966 315 indexUrl = this._index % supervisionUrls.length;
7dde0b73
JB
316 } else {
317 // Get a random url
318 indexUrl = Math.floor(Math.random() * supervisionUrls.length);
319 }
7ec46a9a 320 return supervisionUrls[indexUrl];
7dde0b73 321 }
e118beaa 322 return supervisionUrls as string;
7dde0b73
JB
323 }
324
032d6efc
JB
325 _getReconnectExponentialDelay(): boolean {
326 return !Utils.isUndefined(this._stationInfo.reconnectExponentialDelay) ? this._stationInfo.reconnectExponentialDelay : false;
327 }
328
65c5527e 329 _getAuthorizeRemoteTxRequests(): boolean {
61c2e33d 330 const authorizeRemoteTxRequests = this._getConfigurationKey('AuthorizeRemoteTxRequests');
a6e68f34 331 return authorizeRemoteTxRequests ? Utils.convertToBoolean(authorizeRemoteTxRequests.value) : false;
7dde0b73
JB
332 }
333
65c5527e 334 _getLocalAuthListEnabled(): boolean {
61c2e33d 335 const localAuthListEnabled = this._getConfigurationKey('LocalAuthListEnabled');
def3d48e
JB
336 return localAuthListEnabled ? Utils.convertToBoolean(localAuthListEnabled.value) : false;
337 }
338
136c90ba
JB
339 async _startMessageSequence(): Promise<void> {
340 // Start WebSocket ping
341 this._startWebSocketPing();
5ad8570f 342 // Start heartbeat
6af9012e 343 this._startHeartbeat();
0a60c33c 344 // Initialize connectors status
5ad8570f 345 for (const connector in this._connectors) {
136c90ba
JB
346 if (!this._hasStopped && !this.getConnector(Utils.convertToInt(connector)).status && this.getConnector(Utils.convertToInt(connector)).bootStatus) {
347 // Send status in template at startup
348 await this.sendStatusNotification(Utils.convertToInt(connector), this.getConnector(Utils.convertToInt(connector)).bootStatus);
349 } else if (this._hasStopped && this.getConnector(Utils.convertToInt(connector)).bootStatus) {
350 // Send status in template after reset
351 await this.sendStatusNotification(Utils.convertToInt(connector), this.getConnector(Utils.convertToInt(connector)).bootStatus);
352 } else if (!this._hasStopped && this.getConnector(Utils.convertToInt(connector)).status) {
353 // Send previous status at template reload
354 await this.sendStatusNotification(Utils.convertToInt(connector), this.getConnector(Utils.convertToInt(connector)).status);
5ad8570f 355 } else {
136c90ba
JB
356 // Send default status
357 await this.sendStatusNotification(Utils.convertToInt(connector), ChargePointStatus.AVAILABLE);
5ad8570f
JB
358 }
359 }
0a60c33c 360 // Start the ATG
9ac86a7e 361 if (this._stationInfo.AutomaticTransactionGenerator.enable) {
5ad8570f
JB
362 if (!this._automaticTransactionGeneration) {
363 this._automaticTransactionGeneration = new AutomaticTransactionGenerator(this);
364 }
365 if (this._automaticTransactionGeneration.timeToStop) {
366 this._automaticTransactionGeneration.start();
367 }
368 }
8bce55bf
JB
369 if (this.getEnableStatistics()) {
370 this._statistics.start();
371 }
5ad8570f
JB
372 }
373
9ac86a7e 374 async _stopMessageSequence(reason: StopTransactionReason = StopTransactionReason.NONE): Promise<void> {
136c90ba
JB
375 // Stop WebSocket ping
376 this._stopWebSocketPing();
79411696
JB
377 // Stop heartbeat
378 this._stopHeartbeat();
379 // Stop the ATG
9ac86a7e 380 if (this._stationInfo.AutomaticTransactionGenerator.enable &&
79411696
JB
381 this._automaticTransactionGeneration &&
382 !this._automaticTransactionGeneration.timeToStop) {
383 await this._automaticTransactionGeneration.stop(reason);
384 } else {
385 for (const connector in this._connectors) {
386 if (this.getConnector(Utils.convertToInt(connector)).transactionStarted) {
387 await this.sendStopTransaction(this.getConnector(Utils.convertToInt(connector)).transactionId, reason);
388 }
389 }
390 }
391 }
392
136c90ba
JB
393 _startWebSocketPing(): void {
394 const webSocketPingInterval: number = this._getConfigurationKey('WebSocketPingInterval') ? Utils.convertToInt(this._getConfigurationKey('WebSocketPingInterval').value) : 0;
395 if (webSocketPingInterval > 0 && !this._webSocketPingSetInterval) {
396 this._webSocketPingSetInterval = setInterval(() => {
397 if (this._wsConnection?.readyState === WebSocket.OPEN) {
398 this._wsConnection.ping((): void => {});
399 }
400 }, webSocketPingInterval * 1000);
401 logger.info(this._logPrefix() + ' WebSocket ping started every ' + Utils.secondsToHHMMSS(webSocketPingInterval));
402 } else if (this._webSocketPingSetInterval) {
403 logger.info(this._logPrefix() + ' WebSocket ping every ' + Utils.secondsToHHMMSS(webSocketPingInterval) + ' already started');
404 } else {
405 logger.error(`${this._logPrefix()} WebSocket ping interval set to ${webSocketPingInterval ? Utils.secondsToHHMMSS(webSocketPingInterval) : webSocketPingInterval}, not starting the WebSocket ping`);
406 }
407 }
408
409 _stopWebSocketPing(): void {
410 if (this._webSocketPingSetInterval) {
411 clearInterval(this._webSocketPingSetInterval);
412 this._webSocketPingSetInterval = null;
413 }
414 }
415
416 _restartWebSocketPing(): void {
417 // Stop WebSocket ping
418 this._stopWebSocketPing();
419 // Start WebSocket ping
420 this._startWebSocketPing();
421 }
422
6af9012e
JB
423 _startHeartbeat(): void {
424 if (this._heartbeatInterval && this._heartbeatInterval > 0 && !this._heartbeatSetInterval) {
136c90ba
JB
425 this._heartbeatSetInterval = setInterval(async () => {
426 await this.sendHeartbeat();
6af9012e 427 }, this._heartbeatInterval);
9ac86a7e 428 logger.info(this._logPrefix() + ' Heartbeat started every ' + Utils.milliSecondsToHHMMSS(this._heartbeatInterval));
136c90ba
JB
429 } else if (this._heartbeatSetInterval) {
430 logger.info(this._logPrefix() + ' Heartbeat every ' + Utils.milliSecondsToHHMMSS(this._heartbeatInterval) + ' already started');
7dde0b73 431 } else {
136c90ba 432 logger.error(`${this._logPrefix()} Heartbeat interval set to ${this._heartbeatInterval ? Utils.milliSecondsToHHMMSS(this._heartbeatInterval) : this._heartbeatInterval}, not starting the heartbeat`);
0a60c33c
JB
433 }
434 }
435
65c5527e 436 _stopHeartbeat(): void {
0a60c33c
JB
437 if (this._heartbeatSetInterval) {
438 clearInterval(this._heartbeatSetInterval);
439 this._heartbeatSetInterval = null;
7dde0b73 440 }
5ad8570f
JB
441 }
442
136c90ba
JB
443 _restartHeartbeat(): void {
444 // Stop heartbeat
445 this._stopHeartbeat();
446 // Start heartbeat
447 this._startHeartbeat();
448 }
449
65c5527e
JB
450 _startAuthorizationFileMonitoring(): void {
451 // eslint-disable-next-line @typescript-eslint/no-unused-vars
5ad8570f
JB
452 fs.watchFile(this._getAuthorizationFile(), (current, previous) => {
453 try {
ead548f2 454 logger.debug(this._logPrefix() + ' Authorization file ' + this._getAuthorizationFile() + ' have changed, reload');
5ad8570f
JB
455 // Initialize _authorizedTags
456 this._authorizedTags = this._loadAndGetAuthorizedTags();
457 } catch (error) {
7ec46a9a 458 logger.error(this._logPrefix() + ' Authorization file monitoring error: %j', error);
5ad8570f
JB
459 }
460 });
461 }
462
65c5527e
JB
463 _startStationTemplateFileMonitoring(): void {
464 // eslint-disable-next-line @typescript-eslint/no-unused-vars
5ad8570f
JB
465 fs.watchFile(this._stationTemplateFile, (current, previous) => {
466 try {
ead548f2 467 logger.debug(this._logPrefix() + ' Template file ' + this._stationTemplateFile + ' have changed, reload');
5ad8570f
JB
468 // Initialize
469 this._initialize();
9ac86a7e 470 if (!this._stationInfo.AutomaticTransactionGenerator.enable &&
10570d97
JB
471 this._automaticTransactionGeneration) {
472 this._automaticTransactionGeneration.stop().catch(() => { });
79411696 473 }
136c90ba 474 // FIXME?: restart heartbeat and WebSocket ping when their interval values have changed
5ad8570f 475 } catch (error) {
7ec46a9a 476 logger.error(this._logPrefix() + ' Charging station template file monitoring error: %j', error);
5ad8570f
JB
477 }
478 });
479 }
480
6af9012e 481 _startMeterValues(connectorId: number, interval: number): void {
8bce55bf 482 if (!this.getConnector(connectorId).transactionStarted) {
6ecb15e4 483 logger.error(`${this._logPrefix()} Trying to start MeterValues on connector Id ${connectorId} with no transaction started`);
5ad8570f 484 return;
8bce55bf 485 } else if (this.getConnector(connectorId).transactionStarted && !this.getConnector(connectorId).transactionId) {
6ecb15e4 486 logger.error(`${this._logPrefix()} Trying to start MeterValues on connector Id ${connectorId} with no transaction id`);
5ad8570f
JB
487 return;
488 }
0a60c33c 489 if (interval > 0) {
10570d97 490 this.getConnector(connectorId).transactionSetInterval = setInterval(async () => {
8bce55bf
JB
491 if (this.getEnableStatistics()) {
492 const sendMeterValues = performance.timerify(this.sendMeterValues);
493 this._performanceObserver.observe({
494 entryTypes: ['function'],
495 });
65c5527e 496 await sendMeterValues(connectorId, interval, this);
8bce55bf 497 } else {
65c5527e 498 await this.sendMeterValues(connectorId, interval, this);
8bce55bf 499 }
0a60c33c
JB
500 }, interval);
501 } else {
9ac86a7e 502 logger.error(`${this._logPrefix()} Charging station MeterValueSampleInterval configuration set to ${Utils.milliSecondsToHHMMSS(interval)}, not sending MeterValues`);
0a60c33c 503 }
7dde0b73
JB
504 }
505
032d6efc
JB
506 _openWSConnection(options?: WebSocket.ClientOptions): void {
507 if (Utils.isUndefined(options)) {
508 options = {} as WebSocket.ClientOptions;
509 }
510 if (Utils.isUndefined(options.handshakeTimeout)) {
511 options.handshakeTimeout = this._connectionTimeout;
512 }
513 this._wsConnection = new WebSocket(this._wsConnectionUrl, 'ocpp' + Constants.OCPP_VERSION_16, options);
ead548f2 514 logger.info(this._logPrefix() + ' Will communicate through URL ' + this._supervisionUrl);
136c90ba
JB
515 }
516
517 start(): void {
518 this._openWSConnection();
2e6f5966
JB
519 // Monitor authorization file
520 this._startAuthorizationFileMonitoring();
521 // Monitor station template file
522 this._startStationTemplateFileMonitoring();
7dde0b73
JB
523 // Handle Socket incoming messages
524 this._wsConnection.on('message', this.onMessage.bind(this));
525 // Handle Socket error
526 this._wsConnection.on('error', this.onError.bind(this));
527 // Handle Socket close
528 this._wsConnection.on('close', this.onClose.bind(this));
529 // Handle Socket opening connection
530 this._wsConnection.on('open', this.onOpen.bind(this));
531 // Handle Socket ping
532 this._wsConnection.on('ping', this.onPing.bind(this));
136c90ba
JB
533 // Handle Socket pong
534 this._wsConnection.on('pong', this.onPong.bind(this));
7dde0b73
JB
535 }
536
9ac86a7e 537 async stop(reason: StopTransactionReason = StopTransactionReason.NONE): Promise<void> {
136c90ba 538 // Stop message sequence
10570d97 539 await this._stopMessageSequence(reason);
5ad8570f
JB
540 // eslint-disable-next-line guard-for-in
541 for (const connector in this._connectors) {
6b0ce541 542 await this.sendStatusNotification(Utils.convertToInt(connector), ChargePointStatus.UNAVAILABLE);
5ad8570f 543 }
136c90ba 544 if (this._wsConnection?.readyState === WebSocket.OPEN) {
65c5527e 545 this._wsConnection.close();
5ad8570f 546 }
9ac86a7e 547 this._hasStopped = true;
5ad8570f
JB
548 }
549
032d6efc 550 async _reconnect(error): Promise<void> {
6bf6769e 551 logger.error(this._logPrefix() + ' Socket: abnormally closed: %j', error);
136c90ba
JB
552 // Stop heartbeat
553 this._stopHeartbeat();
5ad8570f 554 // Stop the ATG if needed
9ac86a7e
JB
555 if (this._stationInfo.AutomaticTransactionGenerator.enable &&
556 this._stationInfo.AutomaticTransactionGenerator.stopOnConnectionFailure &&
5ad8570f
JB
557 this._automaticTransactionGeneration &&
558 !this._automaticTransactionGeneration.timeToStop) {
7ec46a9a 559 this._automaticTransactionGeneration.stop().catch(() => { });
5ad8570f 560 }
032d6efc 561 if (this._autoReconnectRetryCount < this._autoReconnectMaxRetries || this._autoReconnectMaxRetries === -1) {
5ad8570f 562 this._autoReconnectRetryCount++;
032d6efc
JB
563 const reconnectDelay = (this._getReconnectExponentialDelay() ? Utils.exponentialDelay(this._autoReconnectRetryCount) : this._connectionTimeout);
564 logger.error(`${this._logPrefix()} Socket: connection retry in ${Utils.roundTo(reconnectDelay, 2)}ms, timeout ${reconnectDelay - 100}ms`);
565 await Utils.sleep(reconnectDelay);
566 logger.error(this._logPrefix() + ' Socket: reconnecting try #' + this._autoReconnectRetryCount.toString());
567 this._openWSConnection({ handshakeTimeout : reconnectDelay - 100 });
568 } else if (this._autoReconnectMaxRetries !== -1) {
569 logger.error(`${this._logPrefix()} Socket: max retries reached (${this._autoReconnectRetryCount}) or retry disabled (${this._autoReconnectMaxRetries})`);
5ad8570f
JB
570 }
571 }
572
136c90ba 573 async onOpen(): Promise<void> {
ead548f2 574 logger.info(`${this._logPrefix()} Is connected to server through ${this._wsConnectionUrl}`);
136c90ba 575 if (!this._hasSocketRestarted || this._hasStopped) {
0bbcb3dc 576 // Send BootNotification
136c90ba 577 await this.sendBootNotification();
0bbcb3dc 578 }
136c90ba 579 await this._startMessageSequence();
9ac86a7e 580 if (this._hasSocketRestarted) {
546dec0f 581 if (!Utils.isEmptyArray(this._messageQueue)) {
63b48f77 582 this._messageQueue.forEach((message, index) => {
136c90ba 583 if (this._wsConnection?.readyState === WebSocket.OPEN) {
63b48f77 584 this._messageQueue.splice(index, 1);
7dde0b73
JB
585 this._wsConnection.send(message);
586 }
587 });
588 }
7dde0b73 589 }
136c90ba 590 this._autoReconnectRetryCount = 0;
032d6efc 591 this._hasSocketRestarted = false;
7dde0b73
JB
592 }
593
032d6efc 594 async onError(errorEvent): Promise<void> {
136c90ba 595 switch (errorEvent.code) {
7dde0b73 596 case 'ECONNREFUSED':
9ac86a7e 597 this._hasSocketRestarted = true;
032d6efc 598 await this._reconnect(errorEvent);
7dde0b73
JB
599 break;
600 default:
a324ad9b 601 logger.error(this._logPrefix() + ' Socket error: %j', errorEvent);
7dde0b73
JB
602 break;
603 }
604 }
605
032d6efc 606 async onClose(closeEvent): Promise<void> {
a324ad9b 607 switch (closeEvent) {
7dde0b73
JB
608 case 1000: // Normal close
609 case 1005:
6bf6769e 610 logger.info(this._logPrefix() + ' Socket normally closed: %j', closeEvent);
7dde0b73
JB
611 this._autoReconnectRetryCount = 0;
612 break;
613 default: // Abnormal close
9ac86a7e 614 this._hasSocketRestarted = true;
032d6efc 615 await this._reconnect(closeEvent);
7dde0b73
JB
616 break;
617 }
618 }
619
6b0ce541 620 onPing(): void {
ead548f2 621 logger.debug(this._logPrefix() + ' Has received a WS ping (rfc6455) from the server');
7dde0b73
JB
622 }
623
136c90ba
JB
624 onPong(): void {
625 logger.debug(this._logPrefix() + ' Has received a WS pong (rfc6455) from the server');
626 }
627
628 async onMessage(messageEvent: MessageEvent): Promise<void> {
2d8cee5a 629 let [messageType, messageId, commandName, commandPayload, errorDetails] = [0, '', Constants.ENTITY_CHARGING_STATION, '', ''];
7dde0b73 630 try {
2d8cee5a 631 // Parse the message
136c90ba 632 [messageType, messageId, commandName, commandPayload, errorDetails] = JSON.parse(messageEvent.toString());
2d8cee5a 633
7dde0b73
JB
634 // Check the Type of message
635 switch (messageType) {
636 // Incoming Message
f7869514 637 case Constants.OCPP_JSON_CALL_MESSAGE:
7f134aca
JB
638 if (this.getEnableStatistics()) {
639 this._statistics.addMessage(commandName, messageType);
640 }
7dde0b73 641 // Process the call
7dde0b73
JB
642 await this.handleRequest(messageId, commandName, commandPayload);
643 break;
644 // Outcome Message
f7869514 645 case Constants.OCPP_JSON_CALL_RESULT_MESSAGE:
7dde0b73
JB
646 // Respond
647 // eslint-disable-next-line no-case-declarations
648 let responseCallback; let requestPayload;
649 if (Utils.isIterable(this._requests[messageId])) {
650 [responseCallback, , requestPayload] = this._requests[messageId];
651 } else {
5933cbc8 652 throw new Error(`Response request for message id ${messageId} is not iterable`);
7dde0b73
JB
653 }
654 if (!responseCallback) {
655 // Error
a979cc12 656 throw new Error(`Response request for unknown message id ${messageId}`);
7dde0b73
JB
657 }
658 delete this._requests[messageId];
7dde0b73
JB
659 responseCallback(commandName, requestPayload);
660 break;
661 // Error Message
f7869514 662 case Constants.OCPP_JSON_CALL_ERROR_MESSAGE:
7dde0b73
JB
663 if (!this._requests[messageId]) {
664 // Error
a979cc12 665 throw new Error(`Error request for unknown message id ${messageId}`);
7dde0b73
JB
666 }
667 // eslint-disable-next-line no-case-declarations
668 let rejectCallback;
669 if (Utils.isIterable(this._requests[messageId])) {
670 [, rejectCallback] = this._requests[messageId];
671 } else {
5933cbc8 672 throw new Error(`Error request for message id ${messageId} is not iterable`);
7dde0b73
JB
673 }
674 delete this._requests[messageId];
675 rejectCallback(new OCPPError(commandName, commandPayload, errorDetails));
676 break;
677 // Error
678 default:
7f134aca
JB
679 // eslint-disable-next-line no-case-declarations
680 const errMsg = `${this._logPrefix()} Wrong message type ${messageType}`;
681 logger.error(errMsg);
682 throw new Error(errMsg);
7dde0b73
JB
683 }
684 } catch (error) {
685 // Log
a324ad9b 686 logger.error('%s Incoming message %j processing error %s on request content type %s', this._logPrefix(), messageEvent, error, this._requests[messageId]);
7dde0b73 687 // Send error
136c90ba 688 messageType !== Constants.OCPP_JSON_CALL_ERROR_MESSAGE && await this.sendError(messageId, error, commandName);
7dde0b73
JB
689 }
690 }
691
6b0ce541 692 async sendHeartbeat(): Promise<void> {
0a60c33c
JB
693 try {
694 const payload = {
695 currentTime: new Date().toISOString(),
696 };
65c5527e 697 await this.sendMessage(Utils.generateUUID(), payload, Constants.OCPP_JSON_CALL_MESSAGE, 'Heartbeat');
0a60c33c 698 } catch (error) {
7ec46a9a 699 logger.error(this._logPrefix() + ' Send Heartbeat error: %j', error);
0a60c33c
JB
700 throw error;
701 }
702 }
703
6b0ce541 704 async sendBootNotification(): Promise<void> {
0a60c33c 705 try {
65c5527e 706 await this.sendMessage(Utils.generateUUID(), this._bootNotificationMessage, Constants.OCPP_JSON_CALL_MESSAGE, 'BootNotification');
0a60c33c 707 } catch (error) {
7ec46a9a 708 logger.error(this._logPrefix() + ' Send BootNotification error: %j', error);
0a60c33c
JB
709 throw error;
710 }
711 }
712
10570d97 713 async sendStatusNotification(connectorId: number, status: ChargePointStatus, errorCode: ChargePointErrorCode = ChargePointErrorCode.NO_ERROR): Promise<void> {
6b0ce541 714 this.getConnector(connectorId).status = status;
5ad8570f
JB
715 try {
716 const payload = {
717 connectorId,
718 errorCode,
719 status,
720 };
721 await this.sendMessage(Utils.generateUUID(), payload, Constants.OCPP_JSON_CALL_MESSAGE, 'StatusNotification');
722 } catch (error) {
7ec46a9a 723 logger.error(this._logPrefix() + ' Send StatusNotification error: %j', error);
5ad8570f 724 throw error;
027b409a
JB
725 }
726 }
727
9ac86a7e 728 async sendStartTransaction(connectorId: number, idTag?: string): Promise<StartTransactionResponse> {
5ad8570f
JB
729 try {
730 const payload = {
bec64e8b 731 connectorId,
9ac86a7e 732 ...!Utils.isUndefined(idTag) ? { idTag } : { idTag: Constants.TRANSACTION_DEFAULT_IDTAG },
5ad8570f
JB
733 meterStart: 0,
734 timestamp: new Date().toISOString(),
735 };
9ac86a7e 736 return await this.sendMessage(Utils.generateUUID(), payload, Constants.OCPP_JSON_CALL_MESSAGE, 'StartTransaction') as StartTransactionResponse;
5ad8570f 737 } catch (error) {
7ec46a9a 738 logger.error(this._logPrefix() + ' Send StartTransaction error: %j', error);
5ad8570f 739 throw error;
7dde0b73
JB
740 }
741 }
742
9ac86a7e 743 async sendStopTransaction(transactionId: number, reason: StopTransactionReason = StopTransactionReason.NONE): Promise<StopTransactionResponse> {
032d6efc 744 const idTag = this._getTransactionIdTag(transactionId);
027b409a 745 try {
38c8fd6c
JB
746 const payload = {
747 transactionId,
9ac86a7e 748 ...!Utils.isUndefined(idTag) && { idTag: idTag },
38c8fd6c
JB
749 meterStop: 0,
750 timestamp: new Date().toISOString(),
6af9012e 751 ...reason && { reason },
38c8fd6c 752 };
9ac86a7e 753 return await this.sendMessage(Utils.generateUUID(), payload, Constants.OCPP_JSON_CALL_MESSAGE, 'StopTransaction') as StartTransactionResponse;
027b409a 754 } catch (error) {
7ec46a9a 755 logger.error(this._logPrefix() + ' Send StopTransaction error: %j', error);
5ad8570f 756 throw error;
027b409a
JB
757 }
758 }
759
79411696 760 // eslint-disable-next-line consistent-this
65c5527e 761 async sendMeterValues(connectorId: number, interval: number, self: ChargingStation, debug = false): Promise<void> {
5ad8570f 762 try {
10570d97
JB
763 const sampledValues: {
764 timestamp: string;
765 sampledValue: MeterValue[];
766 } = {
5ad8570f 767 timestamp: new Date().toISOString(),
7abfea5f 768 sampledValue: [],
5ad8570f 769 };
79411696 770 const meterValuesTemplate = self.getConnector(connectorId).MeterValues;
7abfea5f 771 for (let index = 0; index < meterValuesTemplate.length; index++) {
79411696 772 const connector = self.getConnector(connectorId);
0a60c33c 773 // SoC measurand
10570d97 774 if (meterValuesTemplate[index].measurand && meterValuesTemplate[index].measurand === MeterValueMeasurand.STATE_OF_CHARGE && self._getConfigurationKey('MeterValuesSampledData').value.includes(MeterValueMeasurand.STATE_OF_CHARGE)) {
7abfea5f 775 sampledValues.sampledValue.push({
10570d97 776 ...!Utils.isUndefined(meterValuesTemplate[index].unit) ? { unit: meterValuesTemplate[index].unit } : { unit: MeterValueUnit.PERCENT },
6af9012e 777 ...!Utils.isUndefined(meterValuesTemplate[index].context) && { context: meterValuesTemplate[index].context },
7abfea5f 778 measurand: meterValuesTemplate[index].measurand,
10570d97
JB
779 ...!Utils.isUndefined(meterValuesTemplate[index].location) ? { location: meterValuesTemplate[index].location } : { location: MeterValueLocation.EV },
780 ...!Utils.isUndefined(meterValuesTemplate[index].value) ? { value: meterValuesTemplate[index].value } : { value: Utils.getRandomInt(100).toString() },
7abfea5f
JB
781 });
782 const sampledValuesIndex = sampledValues.sampledValue.length - 1;
10570d97
JB
783 if (Utils.convertToInt(sampledValues.sampledValue[sampledValuesIndex].value) > 100 || debug) {
784 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`);
5ad8570f 785 }
0a60c33c 786 // Voltage measurand
10570d97 787 } else if (meterValuesTemplate[index].measurand && meterValuesTemplate[index].measurand === MeterValueMeasurand.VOLTAGE && self._getConfigurationKey('MeterValuesSampledData').value.includes(MeterValueMeasurand.VOLTAGE)) {
7f134aca 788 const voltageMeasurandValue = Utils.getRandomFloatRounded(self._getVoltageOut() + self._getVoltageOut() * 0.1, self._getVoltageOut() - self._getVoltageOut() * 0.1);
7abfea5f 789 sampledValues.sampledValue.push({
10570d97 790 ...!Utils.isUndefined(meterValuesTemplate[index].unit) ? { unit: meterValuesTemplate[index].unit } : { unit: MeterValueUnit.VOLT },
6af9012e 791 ...!Utils.isUndefined(meterValuesTemplate[index].context) && { context: meterValuesTemplate[index].context },
7abfea5f 792 measurand: meterValuesTemplate[index].measurand,
6af9012e 793 ...!Utils.isUndefined(meterValuesTemplate[index].location) && { location: meterValuesTemplate[index].location },
10570d97 794 ...!Utils.isUndefined(meterValuesTemplate[index].value) ? { value: meterValuesTemplate[index].value } : { value: voltageMeasurandValue.toString() },
7abfea5f 795 });
79411696 796 for (let phase = 1; self._getNumberOfPhases() === 3 && phase <= self._getNumberOfPhases(); phase++) {
e118beaa 797 const voltageValue = Utils.convertToFloat(sampledValues.sampledValue[sampledValues.sampledValue.length - 1].value);
10570d97 798 let phaseValue: string;
b2acff85 799 if (voltageValue >= 0 && voltageValue <= 250) {
7abfea5f 800 phaseValue = `L${phase}-N`;
b2acff85 801 } else if (voltageValue > 250) {
79411696 802 phaseValue = `L${phase}-L${(phase + 1) % self._getNumberOfPhases() !== 0 ? (phase + 1) % self._getNumberOfPhases() : self._getNumberOfPhases()}`;
7abfea5f
JB
803 }
804 sampledValues.sampledValue.push({
10570d97 805 ...!Utils.isUndefined(meterValuesTemplate[index].unit) ? { unit: meterValuesTemplate[index].unit } : { unit: MeterValueUnit.VOLT },
6af9012e 806 ...!Utils.isUndefined(meterValuesTemplate[index].context) && { context: meterValuesTemplate[index].context },
7abfea5f 807 measurand: meterValuesTemplate[index].measurand,
6af9012e 808 ...!Utils.isUndefined(meterValuesTemplate[index].location) && { location: meterValuesTemplate[index].location },
10570d97
JB
809 ...!Utils.isUndefined(meterValuesTemplate[index].value) ? { value: meterValuesTemplate[index].value } : { value: voltageMeasurandValue.toString() },
810 phase: phaseValue as MeterValuePhase,
3f40bc9c
JB
811 });
812 }
fee83021 813 // Power.Active.Import measurand
e118beaa 814 } else if (meterValuesTemplate[index].measurand && meterValuesTemplate[index].measurand === MeterValueMeasurand.POWER_ACTIVE_IMPORT && self._getConfigurationKey('MeterValuesSampledData').value.includes(MeterValueMeasurand.POWER_ACTIVE_IMPORT)) {
fee83021 815 // FIXME: factor out powerDivider checks
79411696 816 if (Utils.isUndefined(self._stationInfo.powerDivider)) {
10570d97 817 const errMsg = `${self._logPrefix()} MeterValues measurand ${meterValuesTemplate[index].measurand ? meterValuesTemplate[index].measurand : MeterValueMeasurand.ENERGY_ACTIVE_IMPORT_REGISTER}: powerDivider is undefined`;
fee83021
JB
818 logger.error(errMsg);
819 throw Error(errMsg);
79411696 820 } else if (self._stationInfo.powerDivider && self._stationInfo.powerDivider <= 0) {
10570d97 821 const errMsg = `${self._logPrefix()} MeterValues measurand ${meterValuesTemplate[index].measurand ? meterValuesTemplate[index].measurand : MeterValueMeasurand.ENERGY_ACTIVE_IMPORT_REGISTER}: powerDivider have zero or below value ${self._stationInfo.powerDivider}`;
fee83021
JB
822 logger.error(errMsg);
823 throw Error(errMsg);
824 }
10570d97
JB
825 const errMsg = `${self._logPrefix()} MeterValues measurand ${meterValuesTemplate[index].measurand ? meterValuesTemplate[index].measurand : MeterValueMeasurand.ENERGY_ACTIVE_IMPORT_REGISTER}: Unknown ${self._getPowerOutType()} powerOutType in template file ${self._stationTemplateFile}, cannot calculate ${meterValuesTemplate[index].measurand ? meterValuesTemplate[index].measurand : MeterValueMeasurand.ENERGY_ACTIVE_IMPORT_REGISTER} measurand value`;
826 const powerMeasurandValues = {} as MeasurandValues;
79411696
JB
827 const maxPower = Math.round(self._stationInfo.maxPower / self._stationInfo.powerDivider);
828 const maxPowerPerPhase = Math.round((self._stationInfo.maxPower / self._stationInfo.powerDivider) / self._getNumberOfPhases());
829 switch (self._getPowerOutType()) {
9ac86a7e 830 case PowerOutType.AC:
fee83021
JB
831 if (Utils.isUndefined(meterValuesTemplate[index].value)) {
832 powerMeasurandValues.L1 = Utils.getRandomFloatRounded(maxPowerPerPhase);
833 powerMeasurandValues.L2 = 0;
834 powerMeasurandValues.L3 = 0;
79411696 835 if (self._getNumberOfPhases() === 3) {
fee83021
JB
836 powerMeasurandValues.L2 = Utils.getRandomFloatRounded(maxPowerPerPhase);
837 powerMeasurandValues.L3 = Utils.getRandomFloatRounded(maxPowerPerPhase);
838 }
10570d97 839 powerMeasurandValues.allPhases = Utils.roundTo(powerMeasurandValues.L1 + powerMeasurandValues.L2 + powerMeasurandValues.L3, 2);
fee83021
JB
840 }
841 break;
9ac86a7e 842 case PowerOutType.DC:
fee83021 843 if (Utils.isUndefined(meterValuesTemplate[index].value)) {
10570d97 844 powerMeasurandValues.allPhases = Utils.getRandomFloatRounded(maxPower);
fee83021
JB
845 }
846 break;
847 default:
848 logger.error(errMsg);
849 throw Error(errMsg);
850 }
851 sampledValues.sampledValue.push({
10570d97 852 ...!Utils.isUndefined(meterValuesTemplate[index].unit) ? { unit: meterValuesTemplate[index].unit } : { unit: MeterValueUnit.WATT },
6af9012e 853 ...!Utils.isUndefined(meterValuesTemplate[index].context) && { context: meterValuesTemplate[index].context },
fee83021 854 measurand: meterValuesTemplate[index].measurand,
6af9012e 855 ...!Utils.isUndefined(meterValuesTemplate[index].location) && { location: meterValuesTemplate[index].location },
10570d97 856 ...!Utils.isUndefined(meterValuesTemplate[index].value) ? { value: meterValuesTemplate[index].value } : { value: powerMeasurandValues.allPhases.toString() },
fee83021
JB
857 });
858 const sampledValuesIndex = sampledValues.sampledValue.length - 1;
10570d97
JB
859 if (Utils.convertToFloat(sampledValues.sampledValue[sampledValuesIndex].value) > maxPower || debug) {
860 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}`);
fee83021 861 }
79411696 862 for (let phase = 1; self._getNumberOfPhases() === 3 && phase <= self._getNumberOfPhases(); phase++) {
fee83021
JB
863 const phaseValue = `L${phase}-N`;
864 sampledValues.sampledValue.push({
10570d97 865 ...!Utils.isUndefined(meterValuesTemplate[index].unit) ? { unit: meterValuesTemplate[index].unit } : { unit: MeterValueUnit.WATT },
6af9012e
JB
866 ...!Utils.isUndefined(meterValuesTemplate[index].context) && { context: meterValuesTemplate[index].context },
867 ...!Utils.isUndefined(meterValuesTemplate[index].measurand) && { measurand: meterValuesTemplate[index].measurand },
868 ...!Utils.isUndefined(meterValuesTemplate[index].location) && { location: meterValuesTemplate[index].location },
7ec46a9a 869 ...!Utils.isUndefined(meterValuesTemplate[index].value) ? { value: meterValuesTemplate[index].value } : { value: powerMeasurandValues[`L${phase}`] as string },
10570d97 870 phase: phaseValue as MeterValuePhase,
fee83021
JB
871 });
872 }
3f40bc9c 873 // Current.Import measurand
10570d97 874 } else if (meterValuesTemplate[index].measurand && meterValuesTemplate[index].measurand === MeterValueMeasurand.CURRENT_IMPORT && self._getConfigurationKey('MeterValuesSampledData').value.includes(MeterValueMeasurand.CURRENT_IMPORT)) {
3f40bc9c 875 // FIXME: factor out powerDivider checks
79411696 876 if (Utils.isUndefined(self._stationInfo.powerDivider)) {
10570d97 877 const errMsg = `${self._logPrefix()} MeterValues measurand ${meterValuesTemplate[index].measurand ? meterValuesTemplate[index].measurand : MeterValueMeasurand.ENERGY_ACTIVE_IMPORT_REGISTER}: powerDivider is undefined`;
3f40bc9c
JB
878 logger.error(errMsg);
879 throw Error(errMsg);
79411696 880 } else if (self._stationInfo.powerDivider && self._stationInfo.powerDivider <= 0) {
10570d97 881 const errMsg = `${self._logPrefix()} MeterValues measurand ${meterValuesTemplate[index].measurand ? meterValuesTemplate[index].measurand : MeterValueMeasurand.ENERGY_ACTIVE_IMPORT_REGISTER}: powerDivider have zero or below value ${self._stationInfo.powerDivider}`;
3f40bc9c
JB
882 logger.error(errMsg);
883 throw Error(errMsg);
884 }
10570d97 885 const errMsg = `${self._logPrefix()} MeterValues measurand ${meterValuesTemplate[index].measurand ? meterValuesTemplate[index].measurand : MeterValueMeasurand.ENERGY_ACTIVE_IMPORT_REGISTER}: Unknown ${self._getPowerOutType()} powerOutType in template file ${self._stationTemplateFile}, cannot calculate ${meterValuesTemplate[index].measurand ? meterValuesTemplate[index].measurand : MeterValueMeasurand.ENERGY_ACTIVE_IMPORT_REGISTER} measurand value`;
9ac86a7e 886 const currentMeasurandValues: MeasurandValues = {} as MeasurandValues;
10570d97 887 let maxAmperage: number;
79411696 888 switch (self._getPowerOutType()) {
9ac86a7e 889 case PowerOutType.AC:
79411696 890 maxAmperage = ElectricUtils.ampPerPhaseFromPower(self._getNumberOfPhases(), self._stationInfo.maxPower / self._stationInfo.powerDivider, self._getVoltageOut());
b2acff85
JB
891 if (Utils.isUndefined(meterValuesTemplate[index].value)) {
892 currentMeasurandValues.L1 = Utils.getRandomFloatRounded(maxAmperage);
893 currentMeasurandValues.L2 = 0;
894 currentMeasurandValues.L3 = 0;
79411696 895 if (self._getNumberOfPhases() === 3) {
b2acff85
JB
896 currentMeasurandValues.L2 = Utils.getRandomFloatRounded(maxAmperage);
897 currentMeasurandValues.L3 = Utils.getRandomFloatRounded(maxAmperage);
898 }
10570d97 899 currentMeasurandValues.allPhases = Utils.roundTo((currentMeasurandValues.L1 + currentMeasurandValues.L2 + currentMeasurandValues.L3) / self._getNumberOfPhases(), 2);
b2acff85
JB
900 }
901 break;
9ac86a7e 902 case PowerOutType.DC:
79411696 903 maxAmperage = ElectricUtils.ampTotalFromPower(self._stationInfo.maxPower / self._stationInfo.powerDivider, self._getVoltageOut());
fee83021 904 if (Utils.isUndefined(meterValuesTemplate[index].value)) {
10570d97 905 currentMeasurandValues.allPhases = Utils.getRandomFloatRounded(maxAmperage);
fee83021 906 }
b2acff85
JB
907 break;
908 default:
909 logger.error(errMsg);
910 throw Error(errMsg);
3f40bc9c
JB
911 }
912 sampledValues.sampledValue.push({
10570d97 913 ...!Utils.isUndefined(meterValuesTemplate[index].unit) ? { unit: meterValuesTemplate[index].unit } : { unit: MeterValueUnit.AMP },
6af9012e 914 ...!Utils.isUndefined(meterValuesTemplate[index].context) && { context: meterValuesTemplate[index].context },
3f40bc9c 915 measurand: meterValuesTemplate[index].measurand,
6af9012e 916 ...!Utils.isUndefined(meterValuesTemplate[index].location) && { location: meterValuesTemplate[index].location },
10570d97 917 ...!Utils.isUndefined(meterValuesTemplate[index].value) ? { value: meterValuesTemplate[index].value } : { value: currentMeasurandValues.allPhases.toString() },
3f40bc9c
JB
918 });
919 const sampledValuesIndex = sampledValues.sampledValue.length - 1;
10570d97
JB
920 if (Utils.convertToFloat(sampledValues.sampledValue[sampledValuesIndex].value) > maxAmperage || debug) {
921 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}`);
3f40bc9c 922 }
79411696 923 for (let phase = 1; self._getNumberOfPhases() === 3 && phase <= self._getNumberOfPhases(); phase++) {
3f40bc9c
JB
924 const phaseValue = `L${phase}`;
925 sampledValues.sampledValue.push({
10570d97 926 ...!Utils.isUndefined(meterValuesTemplate[index].unit) ? { unit: meterValuesTemplate[index].unit } : { unit: MeterValueUnit.AMP },
6af9012e
JB
927 ...!Utils.isUndefined(meterValuesTemplate[index].context) && { context: meterValuesTemplate[index].context },
928 ...!Utils.isUndefined(meterValuesTemplate[index].measurand) && { measurand: meterValuesTemplate[index].measurand },
929 ...!Utils.isUndefined(meterValuesTemplate[index].location) && { location: meterValuesTemplate[index].location },
7ec46a9a 930 ...!Utils.isUndefined(meterValuesTemplate[index].value) ? { value: meterValuesTemplate[index].value } : { value: currentMeasurandValues[phaseValue] as string },
10570d97 931 phase: phaseValue as MeterValuePhase,
7abfea5f
JB
932 });
933 }
0a60c33c 934 // Energy.Active.Import.Register measurand (default)
10570d97 935 } else if (!meterValuesTemplate[index].measurand || meterValuesTemplate[index].measurand === MeterValueMeasurand.ENERGY_ACTIVE_IMPORT_REGISTER) {
38c8fd6c 936 // FIXME: factor out powerDivider checks
79411696 937 if (Utils.isUndefined(self._stationInfo.powerDivider)) {
10570d97 938 const errMsg = `${self._logPrefix()} MeterValues measurand ${meterValuesTemplate[index].measurand ? meterValuesTemplate[index].measurand : MeterValueMeasurand.ENERGY_ACTIVE_IMPORT_REGISTER}: powerDivider is undefined`;
6ecb15e4
JB
939 logger.error(errMsg);
940 throw Error(errMsg);
79411696 941 } else if (self._stationInfo.powerDivider && self._stationInfo.powerDivider <= 0) {
10570d97 942 const errMsg = `${self._logPrefix()} MeterValues measurand ${meterValuesTemplate[index].measurand ? meterValuesTemplate[index].measurand : MeterValueMeasurand.ENERGY_ACTIVE_IMPORT_REGISTER}: powerDivider have zero or below value ${self._stationInfo.powerDivider}`;
6ecb15e4
JB
943 logger.error(errMsg);
944 throw Error(errMsg);
945 }
7abfea5f 946 if (Utils.isUndefined(meterValuesTemplate[index].value)) {
79411696 947 const measurandValue = Utils.getRandomInt(self._stationInfo.maxPower / (self._stationInfo.powerDivider * 3600000) * interval);
9b25a525 948 // Persist previous value in connector
3f40bc9c 949 if (connector && !Utils.isNullOrUndefined(connector.lastEnergyActiveImportRegisterValue) && connector.lastEnergyActiveImportRegisterValue >= 0) {
9b25a525
JB
950 connector.lastEnergyActiveImportRegisterValue += measurandValue;
951 } else {
952 connector.lastEnergyActiveImportRegisterValue = 0;
953 }
5ad8570f 954 }
7abfea5f 955 sampledValues.sampledValue.push({
10570d97 956 ...!Utils.isUndefined(meterValuesTemplate[index].unit) ? { unit: meterValuesTemplate[index].unit } : { unit: MeterValueUnit.WATT_HOUR },
6af9012e
JB
957 ...!Utils.isUndefined(meterValuesTemplate[index].context) && { context: meterValuesTemplate[index].context },
958 ...!Utils.isUndefined(meterValuesTemplate[index].measurand) && { measurand: meterValuesTemplate[index].measurand },
959 ...!Utils.isUndefined(meterValuesTemplate[index].location) && { location: meterValuesTemplate[index].location },
7ec46a9a
JB
960 ...!Utils.isUndefined(meterValuesTemplate[index].value) ? { value: meterValuesTemplate[index].value } :
961 { value: connector.lastEnergyActiveImportRegisterValue.toString() },
7abfea5f
JB
962 });
963 const sampledValuesIndex = sampledValues.sampledValue.length - 1;
79411696 964 const maxConsumption = Math.round(self._stationInfo.maxPower * 3600 / (self._stationInfo.powerDivider * interval));
10570d97
JB
965 if (Utils.convertToFloat(sampledValues.sampledValue[sampledValuesIndex].value) > maxConsumption || debug) {
966 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}`);
5ad8570f 967 }
0a60c33c
JB
968 // Unsupported measurand
969 } else {
10570d97 970 logger.info(`${self._logPrefix()} Unsupported MeterValues measurand ${meterValuesTemplate[index].measurand ? meterValuesTemplate[index].measurand : MeterValueMeasurand.ENERGY_ACTIVE_IMPORT_REGISTER} on connectorId ${connectorId}`);
5ad8570f
JB
971 }
972 }
973
974 const payload = {
bec64e8b 975 connectorId,
79411696 976 transactionId: self.getConnector(connectorId).transactionId,
7abfea5f 977 meterValue: sampledValues,
5ad8570f 978 };
79411696 979 await self.sendMessage(Utils.generateUUID(), payload, Constants.OCPP_JSON_CALL_MESSAGE, 'MeterValues');
5ad8570f 980 } catch (error) {
7ec46a9a 981 logger.error(self._logPrefix() + ' Send MeterValues error: %j', error);
0a60c33c 982 throw error;
5ad8570f
JB
983 }
984 }
985
e118beaa 986 async sendError(messageId: string, err: Error | OCPPError, commandName: string): Promise<unknown> {
7f134aca
JB
987 // Check exception type: only OCPP error are accepted
988 const error = err instanceof OCPPError ? err : new OCPPError(Constants.OCPP_ERROR_INTERNAL_ERROR, err.message, err.stack && err.stack);
5ad8570f 989 // Send error
7f134aca 990 return this.sendMessage(messageId, error, Constants.OCPP_JSON_CALL_ERROR_MESSAGE, commandName);
027b409a
JB
991 }
992
9ac86a7e 993 async sendMessage(messageId: string, commandParams, messageType = Constants.OCPP_JSON_CALL_RESULT_MESSAGE, commandName: string): Promise<any> {
65c5527e 994 // eslint-disable-next-line @typescript-eslint/no-this-alias
7dde0b73 995 const self = this;
6af9012e 996 // Send a message through wsConnection
7dde0b73
JB
997 return new Promise((resolve, reject) => {
998 let messageToSend;
999 // Type of message
1000 switch (messageType) {
1001 // Request
f7869514 1002 case Constants.OCPP_JSON_CALL_MESSAGE:
7dde0b73 1003 // Build request
7f134aca
JB
1004 this._requests[messageId] = [responseCallback, rejectCallback, commandParams];
1005 messageToSend = JSON.stringify([messageType, messageId, commandName, commandParams]);
7dde0b73
JB
1006 break;
1007 // Response
f7869514 1008 case Constants.OCPP_JSON_CALL_RESULT_MESSAGE:
7dde0b73 1009 // Build response
7f134aca 1010 messageToSend = JSON.stringify([messageType, messageId, commandParams]);
7dde0b73
JB
1011 break;
1012 // Error Message
f7869514 1013 case Constants.OCPP_JSON_CALL_ERROR_MESSAGE:
a979cc12 1014 // Build Error Message
7f134aca 1015 messageToSend = JSON.stringify([messageType, messageId, commandParams.code ? commandParams.code : Constants.OCPP_ERROR_GENERIC_ERROR, commandParams.message ? commandParams.message : '', commandParams.details ? commandParams.details : {}]);
7dde0b73
JB
1016 break;
1017 }
2e6f5966 1018 // Check if wsConnection is ready
136c90ba 1019 if (this._wsConnection?.readyState === WebSocket.OPEN) {
7f134aca
JB
1020 if (this.getEnableStatistics()) {
1021 this._statistics.addMessage(commandName, messageType);
1022 }
7dde0b73
JB
1023 // Yes: Send Message
1024 this._wsConnection.send(messageToSend);
1025 } else {
7f134aca
JB
1026 let dups = false;
1027 // Handle dups in buffer
1028 for (const message of this._messageQueue) {
1029 // Same message
1030 if (JSON.stringify(messageToSend) === JSON.stringify(message)) {
1031 dups = true;
1032 break;
1033 }
1034 }
1035 if (!dups) {
1036 // Buffer message
1037 this._messageQueue.push(messageToSend);
1038 }
a979cc12 1039 // Reject it
136c90ba 1040 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 : {}));
7dde0b73 1041 }
a979cc12
JB
1042 // Response?
1043 if (messageType === Constants.OCPP_JSON_CALL_RESULT_MESSAGE) {
7dde0b73
JB
1044 // Yes: send Ok
1045 resolve();
a979cc12
JB
1046 } else if (messageType === Constants.OCPP_JSON_CALL_ERROR_MESSAGE) {
1047 // Send timeout
7f134aca 1048 setTimeout(() => rejectCallback(new OCPPError(commandParams.code ? commandParams.code : Constants.OCPP_ERROR_GENERIC_ERROR, commandParams.message ? commandParams.message : `Timeout for message id '${messageId}' with content '${messageToSend}'`, commandParams.details ? commandParams.details : {})), Constants.OCPP_SOCKET_TIMEOUT);
7dde0b73
JB
1049 }
1050
1051 // Function that will receive the request's response
7f134aca
JB
1052 function responseCallback(payload, requestPayload): void {
1053 if (self.getEnableStatistics()) {
1054 self._statistics.addMessage(commandName, messageType);
1055 }
7dde0b73 1056 // Send the response
a979cc12 1057 self.handleResponse(commandName, payload, requestPayload);
7dde0b73
JB
1058 resolve(payload);
1059 }
1060
1061 // Function that will receive the request's rejection
7f134aca 1062 function rejectCallback(error: OCPPError): void {
8bce55bf 1063 if (self.getEnableStatistics()) {
7f134aca 1064 self._statistics.addMessage(commandName, messageType);
8bce55bf 1065 }
6bf6769e 1066 logger.debug(`${self._logPrefix()} Error: %j occurred when calling command %s with parameters: %j`, error, commandName, commandParams);
7dde0b73
JB
1067 // Build Exception
1068 // eslint-disable-next-line no-empty-function
e118beaa 1069 self._requests[messageId] = [() => { }, () => { }, {}]; // Properly format the request
7dde0b73
JB
1070 // Send error
1071 reject(error);
1072 }
1073 });
1074 }
1075
10570d97 1076 handleResponse(commandName: string, payload, requestPayload): void {
3f40bc9c 1077 const responseCallbackFn = 'handleResponse' + commandName;
6af9012e
JB
1078 if (typeof this[responseCallbackFn] === 'function') {
1079 this[responseCallbackFn](payload, requestPayload);
3f40bc9c 1080 } else {
6af9012e 1081 logger.error(this._logPrefix() + ' Trying to call an undefined response callback function: ' + responseCallbackFn);
3f40bc9c
JB
1082 }
1083 }
1084
10570d97 1085 handleResponseBootNotification(payload, requestPayload): void {
5ad8570f 1086 if (payload.status === 'Accepted') {
136c90ba
JB
1087 this._heartbeatInterval = Utils.convertToInt(payload.interval) * 1000;
1088 this._heartbeatSetInterval ? this._restartHeartbeat() : this._startHeartbeat();
10570d97
JB
1089 this._addConfigurationKey('HeartBeatInterval', payload.interval);
1090 this._addConfigurationKey('HeartbeatInterval', payload.interval, false, false);
9ac86a7e 1091 this._hasStopped && (this._hasStopped = false);
0a60c33c 1092 } else if (payload.status === 'Pending') {
fda4af57 1093 logger.info(this._logPrefix() + ' Charging station in pending state on the central server');
5ad8570f 1094 } else {
ead548f2 1095 logger.info(this._logPrefix() + ' Charging station rejected by the central server');
7dde0b73 1096 }
7dde0b73
JB
1097 }
1098
10570d97 1099 _initTransactionOnConnector(connectorId: number): void {
8bce55bf
JB
1100 this.getConnector(connectorId).transactionStarted = false;
1101 this.getConnector(connectorId).transactionId = null;
1102 this.getConnector(connectorId).idTag = null;
1103 this.getConnector(connectorId).lastEnergyActiveImportRegisterValue = -1;
0a60c33c
JB
1104 }
1105
10570d97 1106 _resetTransactionOnConnector(connectorId: number): void {
bec64e8b 1107 this._initTransactionOnConnector(connectorId);
8bce55bf
JB
1108 if (this.getConnector(connectorId).transactionSetInterval) {
1109 clearInterval(this.getConnector(connectorId).transactionSetInterval);
027b409a
JB
1110 }
1111 }
1112
9ac86a7e 1113 handleResponseStartTransaction(payload: StartTransactionResponse, requestPayload): void {
7ec46a9a
JB
1114 const connectorId = Utils.convertToInt(requestPayload.connectorId);
1115 if (this.getConnector(connectorId).transactionStarted) {
ab24beae 1116 logger.debug(this._logPrefix() + ' Trying to start a transaction on an already used connector ' + connectorId.toString() + ': %j', this.getConnector(connectorId));
2d0e26f5 1117 return;
d3a7883e 1118 }
84393381 1119
9ac86a7e 1120 let transactionConnectorId: number;
7de604f9 1121 for (const connector in this._connectors) {
7ec46a9a 1122 if (Utils.convertToInt(connector) === connectorId) {
9ac86a7e 1123 transactionConnectorId = Utils.convertToInt(connector);
7de604f9 1124 break;
7dde0b73 1125 }
7de604f9
JB
1126 }
1127 if (!transactionConnectorId) {
ab24beae 1128 logger.error(this._logPrefix() + ' Trying to start a transaction on a non existing connector Id ' + connectorId.toString());
7de604f9
JB
1129 return;
1130 }
9ac86a7e 1131 if (payload.idTagInfo?.status === AuthorizationStatus.ACCEPTED) {
7ec46a9a
JB
1132 this.getConnector(connectorId).transactionStarted = true;
1133 this.getConnector(connectorId).transactionId = payload.transactionId;
1134 this.getConnector(connectorId).idTag = requestPayload.idTag;
1135 this.getConnector(connectorId).lastEnergyActiveImportRegisterValue = 0;
1136 this.sendStatusNotification(connectorId, ChargePointStatus.CHARGING).catch(() => { });
1137 logger.info(this._logPrefix() + ' Transaction ' + payload.transactionId.toString() + ' STARTED on ' + this._stationInfo.name + '#' + connectorId.toString() + ' for idTag ' + requestPayload.idTag);
6ecb15e4
JB
1138 if (this._stationInfo.powerSharedByConnectors) {
1139 this._stationInfo.powerDivider++;
1140 }
7de604f9 1141 const configuredMeterValueSampleInterval = this._getConfigurationKey('MeterValueSampleInterval');
7ec46a9a 1142 this._startMeterValues(connectorId,
e118beaa 1143 configuredMeterValueSampleInterval ? Utils.convertToInt(configuredMeterValueSampleInterval.value) * 1000 : 60000);
7dde0b73 1144 } else {
7ec46a9a
JB
1145 logger.error(this._logPrefix() + ' Starting transaction id ' + payload.transactionId.toString() + ' REJECTED with status ' + payload.idTagInfo?.status + ', idTag ' + requestPayload.idTag);
1146 this._resetTransactionOnConnector(connectorId);
1147 this.sendStatusNotification(connectorId, ChargePointStatus.AVAILABLE).catch(() => { });
7dde0b73
JB
1148 }
1149 }
1150
9ac86a7e
JB
1151 handleResponseStopTransaction(payload: StopTransactionResponse, requestPayload): void {
1152 let transactionConnectorId: number;
d3a7883e 1153 for (const connector in this._connectors) {
7ec46a9a 1154 if (this.getConnector(Utils.convertToInt(connector)).transactionId === Utils.convertToInt(requestPayload.transactionId)) {
9ac86a7e 1155 transactionConnectorId = Utils.convertToInt(connector);
d3a7883e
JB
1156 break;
1157 }
1158 }
1159 if (!transactionConnectorId) {
ab24beae 1160 logger.error(this._logPrefix() + ' Trying to stop a non existing transaction ' + requestPayload.transactionId);
7de604f9 1161 return;
d3a7883e 1162 }
9ac86a7e 1163 if (payload.idTagInfo?.status === AuthorizationStatus.ACCEPTED) {
7ec46a9a 1164 this.sendStatusNotification(transactionConnectorId, ChargePointStatus.AVAILABLE).catch(() => { });
6ecb15e4
JB
1165 if (this._stationInfo.powerSharedByConnectors) {
1166 this._stationInfo.powerDivider--;
1167 }
9ac86a7e 1168 logger.info(this._logPrefix() + ' Transaction ' + requestPayload.transactionId + ' STOPPED on ' + this._stationInfo.name + '#' + transactionConnectorId.toString());
d3a7883e 1169 this._resetTransactionOnConnector(transactionConnectorId);
34dcb3b5 1170 } else {
9ac86a7e 1171 logger.error(this._logPrefix() + ' Stopping transaction id ' + requestPayload.transactionId + ' REJECTED with status ' + payload.idTagInfo?.status);
34dcb3b5
JB
1172 }
1173 }
1174
10570d97 1175 handleResponseStatusNotification(payload, requestPayload): void {
ead548f2 1176 logger.debug(this._logPrefix() + ' Status notification response received: %j to StatusNotification request: %j', payload, requestPayload);
7dde0b73
JB
1177 }
1178
10570d97 1179 handleResponseMeterValues(payload, requestPayload): void {
ead548f2 1180 logger.debug(this._logPrefix() + ' MeterValues response received: %j to MeterValues request: %j', payload, requestPayload);
027b409a
JB
1181 }
1182
10570d97 1183 handleResponseHeartbeat(payload, requestPayload): void {
ead548f2 1184 logger.debug(this._logPrefix() + ' Heartbeat response received: %j to Heartbeat request: %j', payload, requestPayload);
7dde0b73
JB
1185 }
1186
e118beaa 1187 async handleRequest(messageId: string, commandName: string, commandPayload): Promise<void> {
3f40bc9c 1188 let response;
7dde0b73 1189 // Call
fda4af57 1190 if (typeof this['handleRequest' + commandName] === 'function') {
7dde0b73 1191 try {
3f40bc9c 1192 // Call the method to build the response
fda4af57 1193 response = await this['handleRequest' + commandName](commandPayload);
7dde0b73
JB
1194 } catch (error) {
1195 // Log
7ec46a9a 1196 logger.error(this._logPrefix() + ' Handle request error: %j', error);
facd8ebd 1197 // Send back response to inform backend
7f134aca
JB
1198 await this.sendError(messageId, error, commandName);
1199 throw error;
7dde0b73
JB
1200 }
1201 } else {
84393381 1202 // Throw exception
64dc325e 1203 await this.sendError(messageId, new OCPPError(Constants.OCPP_ERROR_NOT_IMPLEMENTED, `${commandName} is not implemented`, {}), commandName);
7dde0b73
JB
1204 throw new Error(`${commandName} is not implemented ${JSON.stringify(commandPayload, null, ' ')}`);
1205 }
84393381 1206 // Send response
7f134aca 1207 await this.sendMessage(messageId, response, Constants.OCPP_JSON_CALL_RESULT_MESSAGE, commandName);
7dde0b73
JB
1208 }
1209
fda4af57 1210 // Simulate charging station restart
9ac86a7e 1211 handleRequestReset(commandPayload): DefaultRequestResponse {
5ad8570f 1212 setImmediate(async () => {
9ac86a7e 1213 await this.stop(commandPayload.type + 'Reset' as StopTransactionReason);
0a60c33c 1214 await Utils.sleep(this._stationInfo.resetTime);
5ad8570f
JB
1215 await this.start();
1216 });
9ac86a7e 1217 logger.info(`${this._logPrefix()} ${commandPayload.type} reset command received, simulating it. The station will be back online in ${Utils.milliSecondsToHHMMSS(this._stationInfo.resetTime)}`);
5ad8570f
JB
1218 return Constants.OCPP_RESPONSE_ACCEPTED;
1219 }
1220
9ac86a7e
JB
1221 async handleRequestUnlockConnector(commandPayload): Promise<UnlockResponse> {
1222 const connectorId = Utils.convertToInt(commandPayload.connectorId);
1223 if (connectorId === 0) {
ab24beae 1224 logger.error(this._logPrefix() + ' Trying to unlock connector ' + connectorId.toString());
9ac86a7e
JB
1225 return Constants.OCPP_RESPONSE_UNLOCK_NOT_SUPPORTED;
1226 }
1227 if (this.getConnector(connectorId).transactionStarted) {
1228 const stopResponse = await this.sendStopTransaction(this.getConnector(connectorId).transactionId, StopTransactionReason.UNLOCK_COMMAND);
1229 if (stopResponse.idTagInfo?.status === AuthorizationStatus.ACCEPTED) {
1230 return Constants.OCPP_RESPONSE_UNLOCKED;
1231 }
1232 return Constants.OCPP_RESPONSE_UNLOCK_FAILED;
1233 }
1234 await this.sendStatusNotification(connectorId, ChargePointStatus.AVAILABLE);
1235 return Constants.OCPP_RESPONSE_UNLOCKED;
1236 }
1237
e118beaa 1238 _getConfigurationKey(key: string): ConfigurationKey {
61c2e33d
JB
1239 return this._configuration.configurationKey.find((configElement) => configElement.key === key);
1240 }
1241
10570d97 1242 _addConfigurationKey(key: string, value: string, readonly = false, visible = true, reboot = false): void {
61c2e33d
JB
1243 const keyFound = this._getConfigurationKey(key);
1244 if (!keyFound) {
1245 this._configuration.configurationKey.push({
1246 key,
1247 readonly,
1248 value,
1249 visible,
3497da01 1250 reboot,
61c2e33d
JB
1251 });
1252 }
1253 }
1254
10570d97 1255 _setConfigurationKeyValue(key: string, value: string): void {
61c2e33d
JB
1256 const keyFound = this._getConfigurationKey(key);
1257 if (keyFound) {
d3a7883e
JB
1258 const keyIndex = this._configuration.configurationKey.indexOf(keyFound);
1259 this._configuration.configurationKey[keyIndex].value = value;
61c2e33d
JB
1260 }
1261 }
1262
e118beaa
JB
1263 handleRequestGetConfiguration(commandPayload): { configurationKey: ConfigurationKey[]; unknownKey: string[] } {
1264 const configurationKey: ConfigurationKey[] = [];
1265 const unknownKey: string[] = [];
61c2e33d
JB
1266 if (Utils.isEmptyArray(commandPayload.key)) {
1267 for (const configuration of this._configuration.configurationKey) {
1268 if (Utils.isUndefined(configuration.visible)) {
1269 configuration.visible = true;
61c2e33d
JB
1270 }
1271 if (!configuration.visible) {
1272 continue;
1273 }
1274 configurationKey.push({
1275 key: configuration.key,
1276 readonly: configuration.readonly,
1277 value: configuration.value,
1278 });
facd8ebd 1279 }
61c2e33d 1280 } else {
9ac86a7e
JB
1281 for (const key of commandPayload.key as string[]) {
1282 const keyFound = this._getConfigurationKey(key);
61c2e33d
JB
1283 if (keyFound) {
1284 if (Utils.isUndefined(keyFound.visible)) {
1285 keyFound.visible = true;
61c2e33d
JB
1286 }
1287 if (!keyFound.visible) {
1288 continue;
1289 }
1290 configurationKey.push({
1291 key: keyFound.key,
1292 readonly: keyFound.readonly,
1293 value: keyFound.value,
1294 });
1295 } else {
9ac86a7e 1296 unknownKey.push(key);
61c2e33d 1297 }
facd8ebd 1298 }
facd8ebd
JB
1299 }
1300 return {
1301 configurationKey,
1302 unknownKey,
1303 };
7dde0b73
JB
1304 }
1305
9ac86a7e 1306 handleRequestChangeConfiguration(commandPayload): ConfigurationResponse {
61c2e33d 1307 const keyToChange = this._getConfigurationKey(commandPayload.key);
7d887a1b 1308 if (!keyToChange) {
9ac86a7e
JB
1309 return Constants.OCPP_CONFIGURATION_RESPONSE_NOT_SUPPORTED;
1310 } else if (keyToChange && keyToChange.readonly) {
1311 return Constants.OCPP_CONFIGURATION_RESPONSE_REJECTED;
1312 } else if (keyToChange && !keyToChange.readonly) {
a6e68f34 1313 const keyIndex = this._configuration.configurationKey.indexOf(keyToChange);
136c90ba
JB
1314 let valueChanged = false;
1315 if (this._configuration.configurationKey[keyIndex].value !== commandPayload.value) {
1316 this._configuration.configurationKey[keyIndex].value = commandPayload.value as string;
1317 valueChanged = true;
1318 }
d3a7883e 1319 let triggerHeartbeatRestart = false;
136c90ba 1320 if (keyToChange.key === 'HeartBeatInterval' && valueChanged) {
d3a7883e
JB
1321 this._setConfigurationKeyValue('HeartbeatInterval', commandPayload.value);
1322 triggerHeartbeatRestart = true;
1323 }
136c90ba 1324 if (keyToChange.key === 'HeartbeatInterval' && valueChanged) {
d3a7883e
JB
1325 this._setConfigurationKeyValue('HeartBeatInterval', commandPayload.value);
1326 triggerHeartbeatRestart = true;
1327 }
1328 if (triggerHeartbeatRestart) {
5c68da4d 1329 this._heartbeatInterval = Utils.convertToInt(commandPayload.value) * 1000;
136c90ba
JB
1330 this._restartHeartbeat();
1331 }
1332 if (keyToChange.key === 'WebSocketPingInterval' && valueChanged) {
1333 this._restartWebSocketPing();
5c68da4d 1334 }
9ac86a7e
JB
1335 if (keyToChange.reboot) {
1336 return Constants.OCPP_CONFIGURATION_RESPONSE_REBOOT_REQUIRED;
7d887a1b 1337 }
9ac86a7e 1338 return Constants.OCPP_CONFIGURATION_RESPONSE_ACCEPTED;
7dde0b73 1339 }
7dde0b73
JB
1340 }
1341
9ac86a7e
JB
1342 async handleRequestRemoteStartTransaction(commandPayload): Promise<DefaultRequestResponse> {
1343 const transactionConnectorID: number = commandPayload.connectorId ? Utils.convertToInt(commandPayload.connectorId) : 1;
fda4af57 1344 if (this._getAuthorizeRemoteTxRequests() && this._getLocalAuthListEnabled() && this.hasAuthorizedTags()) {
dcab13bd 1345 // Check if authorized
2e6f5966 1346 if (this._authorizedTags.find((value) => value === commandPayload.idTag)) {
7dde0b73 1347 // Authorization successful start transaction
65c5527e 1348 await this.sendStartTransaction(transactionConnectorID, commandPayload.idTag);
9ac86a7e 1349 logger.debug(this._logPrefix() + ' Transaction remotely STARTED on ' + this._stationInfo.name + '#' + transactionConnectorID.toString() + ' for idTag ' + commandPayload.idTag);
dcab13bd 1350 return Constants.OCPP_RESPONSE_ACCEPTED;
7dde0b73 1351 }
9ac86a7e 1352 logger.error(this._logPrefix() + ' Remote starting transaction REJECTED with status ' + commandPayload.idTagInfo?.status + ', idTag ' + commandPayload.idTag);
dcab13bd 1353 return Constants.OCPP_RESPONSE_REJECTED;
7dde0b73 1354 }
dcab13bd 1355 // No local authorization check required => start transaction
65c5527e 1356 await this.sendStartTransaction(transactionConnectorID, commandPayload.idTag);
9ac86a7e 1357 logger.debug(this._logPrefix() + ' Transaction remotely STARTED on ' + this._stationInfo.name + '#' + transactionConnectorID.toString() + ' for idTag ' + commandPayload.idTag);
027b409a
JB
1358 return Constants.OCPP_RESPONSE_ACCEPTED;
1359 }
1360
9ac86a7e
JB
1361 async handleRequestRemoteStopTransaction(commandPayload): Promise<DefaultRequestResponse> {
1362 const transactionId = Utils.convertToInt(commandPayload.transactionId);
027b409a 1363 for (const connector in this._connectors) {
9ac86a7e
JB
1364 if (this.getConnector(Utils.convertToInt(connector)).transactionId === transactionId) {
1365 await this.sendStopTransaction(transactionId);
d3a7883e 1366 return Constants.OCPP_RESPONSE_ACCEPTED;
027b409a
JB
1367 }
1368 }
ab24beae 1369 logger.info(this._logPrefix() + ' Trying to remote stop a non existing transaction ' + transactionId.toString());
d3a7883e 1370 return Constants.OCPP_RESPONSE_REJECTED;
7dde0b73 1371 }
7dde0b73
JB
1372}
1373