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