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