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