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