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