0fccef4895d0b27a4abb8d51a370269dcb3a81f6
[e-mobility-charging-stations-simulator.git] / src / charging-station / ChargingStation.js
1 const Configuration = require('../utils/Configuration');
2 const logger = require('../utils/Logger');
3 const WebSocket = require('ws');
4 const Constants = require('../utils/Constants');
5 const Utils = require('../utils/Utils');
6 const OCPPError = require('./OcppError');
7 const {v4: uuid} = require('uuid');
8 const AutomaticTransactionGenerator = require('./AutomaticTransactionGenerator');
9 const Statistics = require('../utils/Statistics');
10 const fs = require('fs');
11 const {performance, PerformanceObserver} = require('perf_hooks');
12
13 class ChargingStation {
14 constructor(index, stationTemplate) {
15 this._requests = {};
16 this._autoReconnectRetryCount = 0;
17 this._autoReconnectMaxRetries = Configuration.getAutoReconnectMaxRetries(); // -1 for unlimited
18 this._autoReconnectTimeout = Configuration.getAutoReconnectTimeout() * 1000; // ms, zero for disabling
19 this._isSocketRestart = false;
20 this._stationInfo = this._buildChargingStation(index, stationTemplate);
21 this._statistics = new Statistics(this._stationInfo.name);
22 this._performanceObserver = new PerformanceObserver((list) => {
23 const entry = list.getEntries()[0];
24 this._statistics.logPerformance(entry, 'ChargingStation');
25 this._performanceObserver.disconnect();
26 });
27 this._index = index;
28 this._messageQueue = [];
29 this._bootNotificationMessage = {
30 chargePointModel: this._stationInfo.chargePointModel,
31 chargePointVendor: this._stationInfo.chargePointVendor,
32 };
33 this._configuration = this._getConfiguration(stationTemplate);
34 this._authorizationFile = this._getAuthorizationFile(stationTemplate);
35 this._supervisionUrl = this._getSupervisionURL(index, stationTemplate);
36 }
37
38 _basicFormatLog() {
39 return Utils.basicFormatLog(` ${this._stationInfo.name}:`);
40 }
41
42 // eslint-disable-next-line class-methods-use-this
43 _getConfiguration(stationTemplate) {
44 return stationTemplate.Configuration ? stationTemplate.Configuration : {};
45 }
46
47 // eslint-disable-next-line class-methods-use-this
48 _getAuthorizationFile(stationTemplate) {
49 return stationTemplate.authorizationFile ? stationTemplate.authorizationFile : '';
50 }
51
52 // eslint-disable-next-line class-methods-use-this
53 _getSupervisionURL(index, stationTemplate) {
54 const supervisionUrls = JSON.parse(JSON.stringify(stationTemplate.supervisionURL ? stationTemplate.supervisionURL : Configuration.getSupervisionURLs()));
55 let indexUrl = 0;
56 if (Array.isArray(supervisionUrls)) {
57 if (Configuration.getEquallySupervisionDistribution()) {
58 indexUrl = index % supervisionUrls.length;
59 } else {
60 // Get a random url
61 indexUrl = Math.floor(Math.random() * supervisionUrls.length);
62 }
63 return supervisionUrls[indexUrl];
64 }
65 return supervisionUrls;
66 }
67
68 // eslint-disable-next-line class-methods-use-this
69 _getStationName(index, stationTemplate) {
70 return stationTemplate.fixedName ? stationTemplate.baseName : stationTemplate.baseName + '-' + ('000000000' + index).substr(('000000000' + index).length - 4);
71 }
72
73 _getAuthorizeRemoteTxRequests() {
74 const authorizeRemoteTxRequests = this._configuration.configurationKey.find((configElement) => configElement.key === 'AuthorizeRemoteTxRequests');
75 return authorizeRemoteTxRequests ? Utils.convertToBoolean(authorizeRemoteTxRequests.value) : false;
76 }
77
78 _getLocalAuthListEnabled() {
79 const localAuthListEnabled = this._configuration.configurationKey.find((configElement) => configElement.key === 'LocalAuthListEnabled');
80 return localAuthListEnabled ? Utils.convertToBoolean(localAuthListEnabled.value) : false;
81 }
82
83 _buildChargingStation(index, stationTemplate) {
84 if (Array.isArray(stationTemplate.power)) {
85 stationTemplate.maxPower = stationTemplate.power[Math.floor(Math.random() * stationTemplate.power.length)];
86 } else {
87 stationTemplate.maxPower = stationTemplate.power;
88 }
89 stationTemplate.name = this._getStationName(index, stationTemplate);
90 return stationTemplate;
91 }
92
93 async start() {
94 logger.info(this._basicFormatLog() + ' Will communicate with ' + this._supervisionUrl);
95 this._url = this._supervisionUrl + '/' + this._stationInfo.name;
96 this._wsConnection = new WebSocket(this._url, 'ocpp1.6');
97 if (this._authorizationFile) {
98 try {
99 // load file
100 const fileDescriptor = fs.openSync(this._authorizationFile, 'r');
101 this._authorizedKeys = JSON.parse(fs.readFileSync(fileDescriptor, 'utf8'));
102 fs.closeSync(fileDescriptor);
103 // monitor authorization file
104 // eslint-disable-next-line no-unused-vars
105 fs.watchFile(this._authorizationFile, (current, previous) => {
106 try {
107 // reload file
108 const fileDescriptor = fs.openSync(this._authorizationFile, 'r');
109 this._authorizedKeys = JSON.parse(fs.readFileSync(fileDescriptor, 'utf8'));
110 fs.closeSync(fileDescriptor);
111 } catch (error) {
112 logger.error(this._basicFormatLog() + ' Authorization file error: ' + error);
113 }
114 });
115 } catch (error) {
116 logger.error(this._basicFormatLog() + ' Authorization file error: ' + error);
117 }
118 } else {
119 logger.info(this._basicFormatLog() + ' No authorization file given in template ' + this._stationInfo.baseName);
120 }
121 // Handle Socket incoming messages
122 this._wsConnection.on('message', this.onMessage.bind(this));
123 // Handle Socket error
124 this._wsConnection.on('error', this.onError.bind(this));
125 // Handle Socket close
126 this._wsConnection.on('close', this.onClose.bind(this));
127 // Handle Socket opening connection
128 this._wsConnection.on('open', this.onOpen.bind(this));
129 // Handle Socket ping
130 this._wsConnection.on('ping', this.onPing.bind(this));
131 }
132
133 onOpen() {
134 logger.info(`${this._basicFormatLog()} Is connected to server through ${this._url}`);
135 if (this._isSocketRestart) {
136 this.basicStartMessageSequence();
137 if (this._messageQueue.length > 0) {
138 this._messageQueue.forEach((message) => {
139 if (this._wsConnection.readyState === WebSocket.OPEN) {
140 this._wsConnection.send(message);
141 }
142 });
143 }
144 } else {
145 // At first start, send BootNotification
146 try {
147 this.sendMessage(uuid(), this._bootNotificationMessage, Constants.OCPP_JSON_CALL_MESSAGE, 'BootNotification');
148 } catch (error) {
149 logger.error(this._basicFormatLog() + ' Send boot notification error: ' + error);
150 }
151 }
152 this._autoReconnectRetryCount = 0;
153 this._isSocketRestart = false;
154 }
155
156 onError(error) {
157 switch (error) {
158 case 'ECONNREFUSED':
159 this._isSocketRestart = true;
160 this._reconnect(error);
161 break;
162 default:
163 logger.error(this._basicFormatLog() + ' Socket error: ' + error);
164 break;
165 }
166 }
167
168 onClose(error) {
169 switch (error) {
170 case 1000: // Normal close
171 case 1005:
172 logger.info(this._basicFormatLog() + ' Socket normally closed ' + error);
173 this._autoReconnectRetryCount = 0;
174 break;
175 default: // Abnormal close
176 this._isSocketRestart = true;
177 this._reconnect(error);
178 break;
179 }
180 }
181
182 onPing() {
183 logger.info(this._basicFormatLog() + ' Has received a WS ping (rfc6455) from the server');
184 }
185
186 async onMessage(message) {
187 let [messageType, messageId, commandName, commandPayload, errorDetails] = [0, '', Constants.ENTITY_CHARGING_STATION, '', ''];
188 try {
189 // Parse the message
190 [messageType, messageId, commandName, commandPayload, errorDetails] = JSON.parse(message);
191
192 // Check the Type of message
193 switch (messageType) {
194 // Incoming Message
195 case Constants.OCPP_JSON_CALL_MESSAGE:
196 // Process the call
197 this._statistics.addMessage(commandName);
198 await this.handleRequest(messageId, commandName, commandPayload);
199 break;
200 // Outcome Message
201 case Constants.OCPP_JSON_CALL_RESULT_MESSAGE:
202 // Respond
203 // eslint-disable-next-line no-case-declarations
204 let responseCallback; let requestPayload;
205 if (Utils.isIterable(this._requests[messageId])) {
206 [responseCallback, , requestPayload] = this._requests[messageId];
207 } else {
208 throw new Error(`Response request for unknown message id ${messageId} is not iterable`);
209 }
210 if (!responseCallback) {
211 // Error
212 throw new Error(`Response for unknown message id ${messageId}`);
213 }
214 delete this._requests[messageId];
215 // this._statistics.addMessage(commandName)
216 responseCallback(commandName, requestPayload);
217 break;
218 // Error Message
219 case Constants.OCPP_JSON_CALL_ERROR_MESSAGE:
220 if (!this._requests[messageId]) {
221 // Error
222 throw new Error(`Error for unknown message id ${messageId}`);
223 }
224 // eslint-disable-next-line no-case-declarations
225 let rejectCallback;
226 if (Utils.isIterable(this._requests[messageId])) {
227 [, rejectCallback] = this._requests[messageId];
228 } else {
229 throw new Error(`Error request for unknown message id ${messageId} is not iterable`);
230 }
231 delete this._requests[messageId];
232 rejectCallback(new OCPPError(commandName, commandPayload, errorDetails));
233 break;
234 // Error
235 default:
236 throw new Error(`Wrong message type ${messageType}`);
237 }
238 } catch (error) {
239 // Log
240 logger.error('%s Incoming message %j processing error %s on request content %s', this._basicFormatLog(), message, error, this._requests[messageId]);
241 // Send error
242 // await this.sendError(messageId, error);
243 }
244 }
245
246 _reconnect(error) {
247 logger.error(this._basicFormatLog() + ' Socket: abnormally closed', error);
248 // Stop heartbeat interval
249 if (this._heartbeatSetInterval) {
250 clearInterval(this._heartbeatSetInterval);
251 this._heartbeatSetInterval = null;
252 }
253 // Stop the ATG
254 if (this._stationInfo.AutomaticTransactionGenerator.enable && this._automaticTransactionGeneration &&
255 !this._automaticTransactionGeneration._timeToStop) {
256 this._automaticTransactionGeneration.stop();
257 }
258 if (this._autoReconnectTimeout !== 0 &&
259 (this._autoReconnectRetryCount < this._autoReconnectMaxRetries || this._autoReconnectMaxRetries === -1)) {
260 logger.error(`${this._basicFormatLog()} Socket: connection retry with timeout ${this._autoReconnectTimeout}ms`);
261 this._autoReconnectRetryCount++;
262 setTimeout(() => {
263 logger.error(this._basicFormatLog() + ' Socket: reconnecting try #' + this._autoReconnectRetryCount);
264 this.start();
265 }, this._autoReconnectTimeout);
266 } else if (this._autoReconnectTimeout !== 0 || this._autoReconnectMaxRetries !== -1) {
267 logger.error(`${this._basicFormatLog()} Socket: max retries reached (${this._autoReconnectRetryCount}) or retry disabled (${this._autoReconnectTimeout})`);
268 }
269 }
270
271 send(command, messageType = Constants.OCPP_JSON_CALL_MESSAGE) {
272 // Send Message
273 return this.sendMessage(uuid(), command, messageType);
274 }
275
276 sendError(messageId, err) {
277 // Check exception: only OCPP error are accepted
278 const error = (err instanceof OCPPError ? err : new OCPPError(Constants.OCPP_ERROR_INTERNAL_ERROR, err.message));
279 // Send error
280 return this.sendMessage(messageId, error, Constants.OCPP_JSON_CALL_ERROR_MESSAGE);
281 }
282
283 sendMessage(messageId, command, messageType = Constants.OCPP_JSON_CALL_RESULT_MESSAGE, commandName = '') {
284 // Send a message through wsConnection
285 const self = this;
286 // Create a promise
287 return new Promise((resolve, reject) => {
288 let messageToSend;
289 // Type of message
290 switch (messageType) {
291 // Request
292 case Constants.OCPP_JSON_CALL_MESSAGE:
293 this._statistics.addMessage(commandName);
294 // Build request
295 this._requests[messageId] = [responseCallback, rejectCallback, command];
296 messageToSend = JSON.stringify([messageType, messageId, commandName, command]);
297 break;
298 // Response
299 case Constants.OCPP_JSON_CALL_RESULT_MESSAGE:
300 // Build response
301 messageToSend = JSON.stringify([messageType, messageId, command]);
302 break;
303 // Error Message
304 case Constants.OCPP_JSON_CALL_ERROR_MESSAGE:
305 // Build Message
306 this._statistics.addMessage(`Error ${command.code}`);
307 messageToSend = JSON.stringify([messageType, messageId, command.code ? command.code : Constants.OCPP_ERROR_GENERIC_ERROR, command.message ? command.message : '', command.details ? command.details : {}]);
308 break;
309 }
310 // Check if wsConnection in ready
311 if (this._wsConnection.readyState === WebSocket.OPEN) {
312 // Yes: Send Message
313 this._wsConnection.send(messageToSend);
314 } else {
315 // Buffer message until connection is back
316 this._messageQueue.push(messageToSend);
317 }
318 // Request?
319 if (messageType !== Constants.OCPP_JSON_CALL_MESSAGE) {
320 // Yes: send Ok
321 resolve();
322 } else if (this._wsConnection.readyState === WebSocket.OPEN) {
323 // Send timeout in case connection is open otherwise wait for ever
324 // FIXME: Handle message on timeout
325 setTimeout(() => rejectCallback(`Timeout for message ${messageId}`), Constants.OCPP_SOCKET_TIMEOUT);
326 }
327
328 // Function that will receive the request's response
329 function responseCallback(payload, requestPayload) {
330 self._statistics.addMessage(commandName, true);
331 const responseCallbackFn = 'handleResponse' + commandName;
332 if (typeof self[responseCallbackFn] === 'function') {
333 self[responseCallbackFn](payload, requestPayload, self);
334 } else {
335 // logger.error(this._basicFormatLog() + ' Trying to call an undefined callback function: ' + responseCallbackFn)
336 }
337 // Send the response
338 resolve(payload);
339 }
340
341 // Function that will receive the request's rejection
342 function rejectCallback(reason) {
343 // Build Exception
344 // eslint-disable-next-line no-empty-function
345 self._requests[messageId] = [() => { }, () => { }, '']; // Properly format the request
346 const error = reason instanceof OCPPError ? reason : new Error(reason);
347 // Send error
348 reject(error);
349 }
350 });
351 }
352
353 handleResponseBootNotification(payload) {
354 if (payload.status === 'Accepted') {
355 this._heartbeatInterval = payload.interval * 1000;
356 this.basicStartMessageSequence();
357 }
358 }
359
360 async basicStartMessageSequence() {
361 this._startHeartbeat(this);
362 if (!this._connectors) { // build connectors
363 this._connectors = {};
364 const connectorsConfig = JSON.parse(JSON.stringify(this._stationInfo.Connectors));
365 // determine number of customized connectors
366 let lastConnector;
367 for (lastConnector in connectorsConfig) {
368 if (Utils.convertToInt(lastConnector) === 0 && this._stationInfo.usedConnectorId0) {
369 this._connectors[lastConnector] = connectorsConfig[lastConnector];
370 }
371 }
372 let maxConnectors = 0;
373 if (Array.isArray(this._stationInfo.numberOfConnectors)) {
374 // generate some connectors
375 maxConnectors = this._stationInfo.numberOfConnectors[(this._index - 1) % this._stationInfo.numberOfConnectors.length];
376 } else {
377 maxConnectors = this._stationInfo.numberOfConnectors;
378 }
379 // generate all connectors
380 for (let index = 1; index <= maxConnectors; index++) {
381 const randConnectorID = (this._stationInfo.randomConnectors ? Utils.getRandomInt(lastConnector, 1) : index);
382 this._connectors[index] = connectorsConfig[randConnectorID];
383 }
384 }
385
386 for (const connector in this._connectors) {
387 if (!this._connectors[connector].transactionStarted) {
388 if (this._connectors[connector].bootStatus) {
389 setTimeout(() => this.sendStatusNotification(connector, this._connectors[connector].bootStatus), 500);
390 } else {
391 setTimeout(() => this.sendStatusNotification(connector, 'Available'), 500);
392 }
393 } else {
394 setTimeout(() => this.sendStatusNotification(connector, 'Charging'), 500);
395 }
396 }
397
398 if (this._stationInfo.AutomaticTransactionGenerator.enable) {
399 if (!this._automaticTransactionGeneration) {
400 this._automaticTransactionGeneration = new AutomaticTransactionGenerator(this);
401 }
402 this._automaticTransactionGeneration.start();
403 }
404 this._statistics.start();
405 }
406
407 handleResponseStartTransaction(payload, requestPayload) {
408 // Reset connector transaction related attributes
409 this._connectors[requestPayload.connectorId].transactionStarted = false;
410 this._connectors[requestPayload.connectorId].idTag = requestPayload.idTag;
411
412 if (payload.idTagInfo.status === 'Accepted') {
413 for (const connector in this._connectors) {
414 if (Utils.convertToInt(connector) === Utils.convertToInt(requestPayload.connectorId)) {
415 this._connectors[connector].transactionStarted = true;
416 this._connectors[connector].transactionId = payload.transactionId;
417 this._connectors[connector].lastConsumptionValue = 0;
418 this._connectors[connector].lastSoC = 0;
419 logger.info(this._basicFormatLog() + ' Transaction ' + this._connectors[connector].transactionId + ' STARTED on ' + this._stationInfo.name + '#' + requestPayload.connectorId + ' with idTag ' + requestPayload.idTag);
420 this.sendStatusNotification(requestPayload.connectorId, 'Charging');
421 const configuredMeterValueSampleInterval = this._configuration.configurationKey.find((value) => value.key === 'MeterValueSampleInterval');
422 this.startMeterValues(requestPayload.connectorId,
423 (configuredMeterValueSampleInterval ? configuredMeterValueSampleInterval.value * 1000 : 60000),
424 this);
425 }
426 }
427 } else {
428 logger.error(this._basicFormatLog() + ' Starting transaction id ' + payload.transactionId + ' REJECTED with status ' + payload.idTagInfo.status + ', idTag ' + requestPayload.idTag);
429 for (const connector in this._connectors) {
430 if (Utils.convertToInt(connector) === Utils.convertToInt(requestPayload.connectorId)) {
431 this._resetTransactionOnConnector(connector);
432 }
433 }
434 this.sendStatusNotification(requestPayload.connectorId, 'Available');
435 }
436 }
437
438 async sendStatusNotification(connectorId, status, errorCode = 'NoError') {
439 try {
440 const payload = {
441 connectorId,
442 errorCode,
443 status,
444 };
445 await this.sendMessage(uuid(), payload, Constants.OCPP_JSON_CALL_MESSAGE, 'StatusNotification');
446 } catch (error) {
447 logger.error(this._basicFormatLog() + ' Send status error: ' + error);
448 }
449 }
450
451 // eslint-disable-next-line class-methods-use-this
452 async _startHeartbeat(self) {
453 if (self._heartbeatInterval && !self._heartbeatSetInterval) {
454 logger.info(self._basicFormatLog() + ' Heartbeat started every ' + self._heartbeatInterval + 'ms');
455 self._heartbeatSetInterval = setInterval(() => {
456 try {
457 const payload = {
458 currentTime: new Date().toISOString(),
459 };
460 self.sendMessage(uuid(), payload, Constants.OCPP_JSON_CALL_MESSAGE, 'Heartbeat');
461 } catch (error) {
462 logger.error(self._basicFormatLog() + ' Send heartbeat error: ' + error);
463 }
464 }, self._heartbeatInterval);
465 } else {
466 logger.error(self._basicFormatLog() + ' Heartbeat interval undefined, not starting the heartbeat');
467 }
468 }
469
470 async handleRequest(messageId, commandName, commandPayload) {
471 let result;
472 this._statistics.addMessage(commandName, true);
473 // Call
474 if (typeof this['handle' + commandName] === 'function') {
475 try {
476 // Call the method
477 result = await this['handle' + commandName](commandPayload);
478 } catch (error) {
479 // Log
480 logger.error(this._basicFormatLog() + ' Handle request error: ' + error);
481 // Send back response to inform back end
482 await this.sendError(messageId, error);
483 }
484 } else {
485 // Throw exception
486 await this.sendError(messageId, new OCPPError(Constants.OCPP_ERROR_NOT_IMPLEMENTED, 'Not implemented', {}));
487 throw new Error(`${commandName} is not implemented ${JSON.stringify(commandPayload, null, ' ')}`);
488 }
489 // Send response
490 await this.sendMessage(messageId, result, Constants.OCPP_JSON_CALL_RESULT_MESSAGE);
491 }
492
493 async handleGetConfiguration() {
494 return this._configuration;
495 }
496
497 async handleChangeConfiguration(commandPayload) {
498 const keyToChange = this._configuration.configurationKey.find((element) => element.key === commandPayload.key);
499 if (keyToChange && !Utils.convertToBoolean(keyToChange.readonly)) {
500 const keyIndex = this._configuration.configurationKey.indexOf(keyToChange);
501 this._configuration.configurationKey[keyIndex].value = commandPayload.value;
502 return Constants.OCPP_RESPONSE_ACCEPTED;
503 }
504 return Constants.OCPP_RESPONSE_REJECTED;
505 }
506
507 async handleRemoteStartTransaction(commandPayload) {
508 const transactionConnectorID = (commandPayload.connectorId ? commandPayload.connectorId : '1');
509 if (this.hasAuthorizationKeys() && this._getLocalAuthListEnabled() && this._getAuthorizeRemoteTxRequests()) {
510 // Check if authorized
511 if (this._authorizedKeys.find((value) => value === commandPayload.idTag)) {
512 // Authorization successful start transaction
513 setTimeout(() => this.sendStartTransaction(transactionConnectorID, commandPayload.idTag), 500);
514 logger.info(this._basicFormatLog() + ' Transaction remotely STARTED on ' + this._stationInfo.name + '#' + transactionConnectorID + ' with idTag ' + commandPayload.idTag);
515 return Constants.OCPP_RESPONSE_ACCEPTED;
516 }
517 // Start authorization checks
518 logger.error(this._basicFormatLog() + ' Remote starting transaction REJECTED with status ' + commandPayload.idTagInfo.status + ', idTag ' + commandPayload.idTag);
519 return Constants.OCPP_RESPONSE_REJECTED;
520 }
521 // No local authorization check required => start transaction
522 setTimeout(() => this.sendStartTransaction(transactionConnectorID, commandPayload.idTag), 500);
523 logger.info(this._basicFormatLog() + ' Transaction remotely STARTED on ' + this._stationInfo.name + '#' + transactionConnectorID + ' with idTag ' + commandPayload.idTag);
524 return Constants.OCPP_RESPONSE_ACCEPTED;
525 }
526
527 async sendStartTransaction(connectorID, idTag) {
528 try {
529 const payload = {
530 connectorId: connectorID,
531 idTag,
532 meterStart: 0,
533 timestamp: new Date().toISOString(),
534 };
535 return await this.sendMessage(uuid(), payload, Constants.OCPP_JSON_CALL_MESSAGE, 'StartTransaction');
536 } catch (error) {
537 logger.error(this._basicFormatLog() + ' Send start transaction error: ' + error);
538 this._resetTransactionOnConnector(connectorID);
539 throw error;
540 }
541 }
542
543 async sendStopTransaction(transactionId, connectorID) {
544 try {
545 const payload = {
546 transactionId,
547 meterStop: 0,
548 timestamp: new Date().toISOString(),
549 };
550 await this.sendMessage(uuid(), payload, Constants.OCPP_JSON_CALL_MESSAGE, 'StopTransaction');
551 logger.info(this._basicFormatLog() + ' Transaction ' + this._connectors[connectorID].transactionId + ' STOPPED on ' + this._stationInfo.name + '#' + connectorID);
552 this.sendStatusNotification(connectorID, 'Available');
553 } catch (error) {
554 logger.error(this._basicFormatLog() + ' Send stop transaction error: ' + error);
555 throw error;
556 } finally {
557 this._resetTransactionOnConnector(connectorID);
558 }
559 }
560
561 _resetTransactionOnConnector(connectorID) {
562 this._connectors[connectorID].transactionStarted = false;
563 this._connectors[connectorID].transactionId = null;
564 this._connectors[connectorID].lastConsumptionValue = -1;
565 this._connectors[connectorID].lastSoC = -1;
566 if (this._connectors[connectorID].transactionInterval) {
567 clearInterval(this._connectors[connectorID].transactionInterval);
568 }
569 }
570
571 // eslint-disable-next-line class-methods-use-this
572 async sendMeterValues(connectorID, interval, self) {
573 try {
574 const sampledValueLcl = {
575 timestamp: new Date().toISOString(),
576 };
577 const meterValuesClone = JSON.parse(JSON.stringify(self._getConnector(connectorID).MeterValues));
578 if (Array.isArray(meterValuesClone)) {
579 sampledValueLcl.sampledValue = meterValuesClone;
580 } else {
581 sampledValueLcl.sampledValue = [meterValuesClone];
582 }
583 for (let index = 0; index < sampledValueLcl.sampledValue.length; index++) {
584 if (sampledValueLcl.sampledValue[index].measurand && sampledValueLcl.sampledValue[index].measurand === 'SoC') {
585 sampledValueLcl.sampledValue[index].value = Math.floor(Math.random() * 100) + 1;
586 if (sampledValueLcl.sampledValue[index].value > 100) {
587 logger.info(self._basicFormatLog() + ' Meter type: ' +
588 (sampledValueLcl.sampledValue[index].measurand ? sampledValueLcl.sampledValue[index].measurand : 'default') +
589 ' value: ' + sampledValueLcl.sampledValue[index].value);
590 }
591 } else {
592 // Persist previous value in connector
593 const connector = self._connectors[connectorID];
594 let consumption;
595 consumption = Utils.getRandomInt(self._stationInfo.maxPower / 3600000 * interval, 4);
596 if (connector && connector.lastConsumptionValue >= 0) {
597 connector.lastConsumptionValue += consumption;
598 } else {
599 connector.lastConsumptionValue = 0;
600 }
601 consumption = Math.round(connector.lastConsumptionValue * 3600 / interval);
602 logger.info(self._basicFormatLog() + ' ConnectorID ' + connectorID + ' transaction ' + connector.transactionId + ' value ' + connector.lastConsumptionValue);
603 sampledValueLcl.sampledValue[index].value = connector.lastConsumptionValue;
604 if (sampledValueLcl.sampledValue[index].value > (self._stationInfo.maxPower * 3600 / interval) || sampledValueLcl.sampledValue[index].value < 500) {
605 logger.info(self._basicFormatLog() + ' Meter type: ' +
606 (sampledValueLcl.sampledValue[index].measurand ? sampledValueLcl.sampledValue[index].measurand : 'default') +
607 ' value: ' + sampledValueLcl.sampledValue[index].value + '/' + (self._stationInfo.maxPower * 3600 / interval));
608 }
609 }
610 }
611
612 const payload = {
613 connectorId: connectorID,
614 transactionId: self._connectors[connectorID].transactionId,
615 meterValue: [sampledValueLcl],
616 };
617 await self.sendMessage(uuid(), payload, Constants.OCPP_JSON_CALL_MESSAGE, 'MeterValues');
618 } catch (error) {
619 logger.error(self._basicFormatLog() + ' Send meter values error: ' + error);
620 }
621 }
622
623 async startMeterValues(connectorID, interval, self) {
624 if (!this._connectors[connectorID].transactionStarted) {
625 logger.debug(`${self._basicFormatLog()} Trying to start meter values on connector ID ${connectorID} with no transaction`);
626 } else if (this._connectors[connectorID].transactionStarted && !this._connectors[connectorID].transactionId) {
627 logger.debug(`${self._basicFormatLog()} Trying to start meter values on connector ID ${connectorID} with no transaction id`);
628 }
629 this._connectors[connectorID].transactionInterval = setInterval(async () => {
630 const sendMeterValues = performance.timerify(this.sendMeterValues);
631 this._performanceObserver.observe({
632 entryTypes: ['function'],
633 });
634 await sendMeterValues(connectorID, interval, self);
635 }, interval);
636 }
637
638 async handleRemoteStopTransaction(commandPayload) {
639 for (const connector in this._connectors) {
640 if (this._connectors[connector].transactionId === commandPayload.transactionId) {
641 this.sendStopTransaction(commandPayload.transactionId, connector);
642 }
643 }
644 return Constants.OCPP_RESPONSE_ACCEPTED;
645 }
646
647 hasAuthorizationKeys() {
648 return this._authorizedKeys && this._authorizedKeys.length > 0;
649 }
650
651 getRandomTagId() {
652 const index = Math.round(Math.floor(Math.random() * this._authorizedKeys.length - 1));
653 return this._authorizedKeys[index];
654 }
655
656 _getConnector(number) {
657 return this._stationInfo.Connectors[number];
658 }
659 }
660
661 module.exports = ChargingStation;