Add runtime persitence on OCPP parameters.
[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');
a6e68f34 75 return authorizeRemoteTxRequests ? Utils.convertToBoolean(authorizeRemoteTxRequests.value) : false;
7dde0b73
JB
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) {
a6e68f34 404 // Reset connector transaction related attributes
84393381
JB
405 this._connectors[requestPayload.connectorId].transactionStarted = false;
406 this._connectors[requestPayload.connectorId].idTag = requestPayload.idTag;
407
7dde0b73
JB
408 if (payload.idTagInfo.status === 'Accepted') {
409 for (const connector in this._connectors) {
84393381 410 if (Utils.convertToInt(connector) === requestPayload.connectorId) {
7dde0b73
JB
411 this._connectors[connector].transactionStarted = true;
412 this._connectors[connector].transactionId = payload.transactionId;
413 this._connectors[connector].lastConsumptionValue = 0;
414 this._connectors[connector].lastSoC = 0;
84393381 415 logger.info(this._basicFormatLog() + ' Transaction ' + this._connectors[connector].transactionId + ' STARTED on ' + this._stationInfo.name + '#' + requestPayload.connectorId + ' with idTag ' + requestPayload.idTag);
7dde0b73 416 this.sendStatusNotification(requestPayload.connectorId, 'Charging');
a6e68f34 417 const configuredMeterValueSampleInterval = this._configuration.configurationKey.find((value) => value.key === 'MeterValueSampleInterval');
7dde0b73 418 this.startMeterValues(requestPayload.connectorId,
a6e68f34 419 (configuredMeterValueSampleInterval ? configuredMeterValueSampleInterval.value * 1000 : 60000),
7dde0b73
JB
420 this);
421 }
422 }
423 } else {
424 logger.error(this._basicFormatLog() + ' Starting transaction id ' + payload.transactionId + ' REJECTED with status ' + payload.idTagInfo.status + ', idTag ' + requestPayload.idTag);
425 for (const connector in this._connectors) {
84393381 426 if (Utils.convertToInt(connector) === requestPayload.connectorId) {
7dde0b73
JB
427 this._resetTransactionOnConnector(connector);
428 }
429 }
430 this.sendStatusNotification(requestPayload.connectorId, 'Available');
431 }
432 }
433
434 async sendStatusNotification(connectorId, status, errorCode = 'NoError') {
435 try {
436 const payload = {
437 connectorId,
438 errorCode,
439 status,
440 };
f7869514 441 await this.sendMessage(uuid(), payload, Constants.OCPP_JSON_CALL_MESSAGE, 'StatusNotification');
7dde0b73
JB
442 } catch (error) {
443 logger.error(this._basicFormatLog() + ' Send status error: ' + error);
444 }
445 }
446
447 // eslint-disable-next-line class-methods-use-this
448 async _startHeartbeat(self) {
449 if (self._heartbeatInterval && !self._heartbeatSetInterval) {
450 logger.info(self._basicFormatLog() + ' Heartbeat started every ' + self._heartbeatInterval + 'ms');
451 self._heartbeatSetInterval = setInterval(() => {
452 try {
453 const payload = {
454 currentTime: new Date().toISOString(),
455 };
f7869514 456 self.sendMessage(uuid(), payload, Constants.OCPP_JSON_CALL_MESSAGE, 'Heartbeat');
7dde0b73
JB
457 } catch (error) {
458 logger.error(self._basicFormatLog() + ' Send heartbeat error: ' + error);
459 }
460 }, self._heartbeatInterval);
461 } else {
462 logger.error(self._basicFormatLog() + ' Heartbeat interval undefined, not starting the heartbeat');
463 }
464 }
465
466 async handleRequest(messageId, commandName, commandPayload) {
467 let result;
468 this._statistics.addMessage(commandName, true);
469 // Call
470 if (typeof this['handle' + commandName] === 'function') {
471 try {
472 // Call the method
473 result = await this['handle' + commandName](commandPayload);
474 } catch (error) {
475 // Log
476 logger.error(this._basicFormatLog() + ' Handle request error: ' + error);
477 // Send back response to inform back end
478 await this.sendError(messageId, error);
479 }
480 } else {
84393381 481 // Throw exception
f7869514 482 await this.sendError(messageId, new OCPPError(Constants.OCPP_ERROR_NOT_IMPLEMENTED, 'Not implemented', {}));
7dde0b73
JB
483 throw new Error(`${commandName} is not implemented ${JSON.stringify(commandPayload, null, ' ')}`);
484 }
84393381 485 // Send response
f7869514 486 await this.sendMessage(messageId, result, Constants.OCPP_JSON_CALL_RESULT_MESSAGE);
7dde0b73
JB
487 }
488
489 async handleGetConfiguration() {
490 return this._configuration;
491 }
492
493 async handleChangeConfiguration(commandPayload) {
494 const keyToChange = this._configuration.configurationKey.find((element) => element.key === commandPayload.key);
a6e68f34
JB
495 if (keyToChange && !Utils.convertToBoolean(keyToChange.readonly)) {
496 const keyIndex = this._configuration.configurationKey.indexOf(keyToChange);
497 this._configuration.configurationKey[keyIndex].value = commandPayload.value;
dcab13bd 498 return Constants.OCPP_RESPONSE_ACCEPTED;
7dde0b73 499 }
dcab13bd 500 return Constants.OCPP_RESPONSE_REJECTED;
7dde0b73
JB
501 }
502
503 async handleRemoteStartTransaction(commandPayload) {
504 const transactionConnectorID = (commandPayload.connectorId ? commandPayload.connectorId : '1');
505 if (this.isAuthorizationRequested() && this._authorizeRemoteTxRequests) {
dcab13bd 506 // Check if authorized
7dde0b73
JB
507 if (this._authorizedKeys.find((value) => value === commandPayload.idTag)) {
508 // Authorization successful start transaction
509 setTimeout(() => this.sendStartTransaction(transactionConnectorID, commandPayload.idTag), 500);
84393381 510 logger.info(this._basicFormatLog() + ' Transaction remotely STARTED on ' + this._stationInfo.name + '#' + transactionConnectorID + ' with idTag ' + commandPayload.idTag);
dcab13bd 511 return Constants.OCPP_RESPONSE_ACCEPTED;
7dde0b73
JB
512 }
513 // Start authorization checks
84393381 514 logger.error(this._basicFormatLog() + ' Remote starting transaction REJECTED with status ' + commandPayload.idTagInfo.status + ', idTag ' + commandPayload.idTag);
dcab13bd 515 return Constants.OCPP_RESPONSE_REJECTED;
7dde0b73 516 }
dcab13bd 517 // No local authorization check required => start transaction
7dde0b73 518 setTimeout(() => this.sendStartTransaction(transactionConnectorID, commandPayload.idTag), 500);
84393381 519 logger.info(this._basicFormatLog() + ' Transaction remotely STARTED on ' + this._stationInfo.name + '#' + transactionConnectorID + ' with idTag ' + commandPayload.idTag);
dcab13bd 520 return Constants.OCPP_RESPONSE_ACCEPTED;
7dde0b73
JB
521 }
522
523 async sendStartTransaction(connectorID, idTag) {
524 try {
525 const payload = {
526 connectorId: connectorID,
527 idTag,
528 meterStart: 0,
529 timestamp: new Date().toISOString(),
530 };
f7869514 531 return await this.sendMessage(uuid(), payload, Constants.OCPP_JSON_CALL_MESSAGE, 'StartTransaction');
7dde0b73
JB
532 } catch (error) {
533 logger.error(this._basicFormatLog() + ' Send start transaction error: ' + error);
534 this._resetTransactionOnConnector(connectorID);
535 throw error;
536 }
537 }
538
539 async sendStopTransaction(transactionId, connectorID) {
540 try {
541 const payload = {
542 transactionId,
543 meterStop: 0,
544 timestamp: new Date().toISOString(),
545 };
f7869514 546 await this.sendMessage(uuid(), payload, Constants.OCPP_JSON_CALL_MESSAGE, 'StopTransaction');
7dde0b73
JB
547 logger.info(this._basicFormatLog() + ' Transaction ' + this._connectors[connectorID].transactionId + ' STOPPED on ' + this._stationInfo.name + '#' + connectorID);
548 this.sendStatusNotification(connectorID, 'Available');
549 } catch (error) {
550 logger.error(this._basicFormatLog() + ' Send stop transaction error: ' + error);
551 throw error;
552 } finally {
553 this._resetTransactionOnConnector(connectorID);
554 }
555 }
556
557 _resetTransactionOnConnector(connectorID) {
558 this._connectors[connectorID].transactionStarted = false;
559 this._connectors[connectorID].transactionId = null;
560 this._connectors[connectorID].lastConsumptionValue = -1;
561 this._connectors[connectorID].lastSoC = -1;
562 if (this._connectors[connectorID].transactionInterval) {
563 clearInterval(this._connectors[connectorID].transactionInterval);
564 }
565 }
566
567 // eslint-disable-next-line class-methods-use-this
568 async sendMeterValues(connectorID, interval, self) {
569 try {
570 const sampledValueLcl = {
571 timestamp: new Date().toISOString(),
572 };
573 const meterValuesClone = JSON.parse(JSON.stringify(self._getConnector(connectorID).MeterValues));
574 if (Array.isArray(meterValuesClone)) {
575 sampledValueLcl.sampledValue = meterValuesClone;
576 } else {
577 sampledValueLcl.sampledValue = [meterValuesClone];
578 }
579 for (let index = 0; index < sampledValueLcl.sampledValue.length; index++) {
580 if (sampledValueLcl.sampledValue[index].measurand && sampledValueLcl.sampledValue[index].measurand === 'SoC') {
581 sampledValueLcl.sampledValue[index].value = Math.floor(Math.random() * 100) + 1;
582 if (sampledValueLcl.sampledValue[index].value > 100) {
583 logger.info(self._basicFormatLog() + ' Meter type: ' +
584 (sampledValueLcl.sampledValue[index].measurand ? sampledValueLcl.sampledValue[index].measurand : 'default') +
585 ' value: ' + sampledValueLcl.sampledValue[index].value);
586 }
587 } else {
588 // Persist previous value in connector
589 const connector = self._connectors[connectorID];
590 let consumption;
a6e68f34 591 consumption = Utils.getRandomInt(self._stationInfo.maxPower / 3600000 * interval, 4);
7dde0b73
JB
592 if (connector && connector.lastConsumptionValue >= 0) {
593 connector.lastConsumptionValue += consumption;
594 } else {
595 connector.lastConsumptionValue = 0;
596 }
597 consumption = Math.round(connector.lastConsumptionValue * 3600 / interval);
598 logger.info(self._basicFormatLog() + ' ConnectorID ' + connectorID + ' transaction ' + connector.transactionId + ' value ' + connector.lastConsumptionValue);
599 sampledValueLcl.sampledValue[index].value = connector.lastConsumptionValue;
600 if (sampledValueLcl.sampledValue[index].value > (self._stationInfo.maxPower * 3600 / interval) || sampledValueLcl.sampledValue[index].value < 500) {
601 logger.info(self._basicFormatLog() + ' Meter type: ' +
602 (sampledValueLcl.sampledValue[index].measurand ? sampledValueLcl.sampledValue[index].measurand : 'default') +
603 ' value: ' + sampledValueLcl.sampledValue[index].value + '/' + (self._stationInfo.maxPower * 3600 / interval));
604 }
605 }
606 }
607
608 const payload = {
609 connectorId: connectorID,
610 transactionId: self._connectors[connectorID].transactionId,
611 meterValue: [sampledValueLcl],
612 };
f7869514 613 await self.sendMessage(uuid(), payload, Constants.OCPP_JSON_CALL_MESSAGE, 'MeterValues');
7dde0b73
JB
614 } catch (error) {
615 logger.error(self._basicFormatLog() + ' Send meter values error: ' + error);
616 }
617 }
618
619 async startMeterValues(connectorID, interval, self) {
84393381
JB
620 if (!this._connectors[connectorID].transactionStarted) {
621 logger.debug(`${self._basicFormatLog()} Trying to start meter values on connector ID ${connectorID} with no transaction`);
622 } else if (this._connectors[connectorID].transactionStarted && !this._connectors[connectorID].transactionId) {
623 logger.debug(`${self._basicFormatLog()} Trying to start meter values on connector ID ${connectorID} with no transaction id`);
624 }
7dde0b73
JB
625 this._connectors[connectorID].transactionInterval = setInterval(async () => {
626 const sendMeterValues = performance.timerify(this.sendMeterValues);
627 this._performanceObserver.observe({
628 entryTypes: ['function'],
629 });
630 await sendMeterValues(connectorID, interval, self);
631 }, interval);
632 }
633
634 async handleRemoteStopTransaction(commandPayload) {
635 for (const connector in this._connectors) {
636 if (this._connectors[connector].transactionId === commandPayload.transactionId) {
637 this.sendStopTransaction(commandPayload.transactionId, connector);
638 }
639 }
dcab13bd 640 return Constants.OCPP_RESPONSE_ACCEPTED;
7dde0b73
JB
641 }
642
643 isAuthorizationRequested() {
644 return this._authorizedKeys && this._authorizedKeys.length > 0;
645 }
646
647 getRandomTagId() {
648 const index = Math.round(Math.floor(Math.random() * this._authorizedKeys.length - 1));
649 return this._authorizedKeys[index];
650 }
651
652 _getConnector(number) {
653 return this._stationInfo.Connectors[number];
654 }
655}
656
657module.exports = ChargingStation;