Improve debug logging.
[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 26
83045896 27 this._authorizedTags = this._loadAndGetAuthorizedTags();
2e6f5966
JB
28 }
29
30 _initialize() {
31 this._stationInfo = this._buildStationInfo();
32 this._bootNotificationMessage = {
33 chargePointModel: this._stationInfo.chargePointModel,
34 chargePointVendor: this._stationInfo.chargePointVendor,
6958152c 35 chargePointSerialNumber: this._stationInfo.chargePointSerialNumberPrefix ? this._stationInfo.chargePointSerialNumberPrefix : '',
34dcb3b5 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
83045896 60 _loadAndGetAuthorizedTags() {
2e6f5966
JB
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
83045896 84 this._authorizedTags = this._loadAndGetAuthorizedTags();
2e6f5966
JB
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() {
61c2e33d 124 const authorizeRemoteTxRequests = this._getConfigurationKey('AuthorizeRemoteTxRequests');
a6e68f34 125 return authorizeRemoteTxRequests ? Utils.convertToBoolean(authorizeRemoteTxRequests.value) : false;
7dde0b73
JB
126 }
127
def3d48e 128 _getLocalAuthListEnabled() {
61c2e33d 129 const localAuthListEnabled = this._getConfigurationKey('LocalAuthListEnabled');
def3d48e
JB
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}`);
0bbcb3dc
JB
175 if (!this._heartbeatInterval) {
176 // Send BootNotification
177 try {
178 this.sendMessage(Utils.generateUUID(), this._bootNotificationMessage, Constants.OCPP_JSON_CALL_MESSAGE, 'BootNotification');
179 } catch (error) {
180 logger.error(this._basicFormatLog() + ' Send boot notification error: ' + error);
181 }
182 }
7dde0b73 183 if (this._isSocketRestart) {
027b409a 184 this._basicStartMessageSequence();
7dde0b73
JB
185 if (this._messageQueue.length > 0) {
186 this._messageQueue.forEach((message) => {
187 if (this._wsConnection.readyState === WebSocket.OPEN) {
188 this._wsConnection.send(message);
189 }
190 });
191 }
7dde0b73
JB
192 }
193 this._autoReconnectRetryCount = 0;
194 this._isSocketRestart = false;
195 }
196
197 onError(error) {
198 switch (error) {
199 case 'ECONNREFUSED':
200 this._isSocketRestart = true;
201 this._reconnect(error);
202 break;
203 default:
204 logger.error(this._basicFormatLog() + ' Socket error: ' + error);
205 break;
206 }
207 }
208
209 onClose(error) {
210 switch (error) {
211 case 1000: // Normal close
212 case 1005:
213 logger.info(this._basicFormatLog() + ' Socket normally closed ' + error);
214 this._autoReconnectRetryCount = 0;
215 break;
216 default: // Abnormal close
217 this._isSocketRestart = true;
218 this._reconnect(error);
219 break;
220 }
221 }
222
223 onPing() {
027b409a 224 logger.debug(this._basicFormatLog() + ' Has received a WS ping (rfc6455) from the server');
7dde0b73
JB
225 }
226
227 async onMessage(message) {
2d8cee5a 228 let [messageType, messageId, commandName, commandPayload, errorDetails] = [0, '', Constants.ENTITY_CHARGING_STATION, '', ''];
7dde0b73 229 try {
2d8cee5a
JB
230 // Parse the message
231 [messageType, messageId, commandName, commandPayload, errorDetails] = JSON.parse(message);
232
7dde0b73
JB
233 // Check the Type of message
234 switch (messageType) {
235 // Incoming Message
f7869514 236 case Constants.OCPP_JSON_CALL_MESSAGE:
7dde0b73 237 // Process the call
7dde0b73
JB
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];
7dde0b73
JB
255 responseCallback(commandName, requestPayload);
256 break;
257 // Error Message
f7869514 258 case Constants.OCPP_JSON_CALL_ERROR_MESSAGE:
7dde0b73
JB
259 if (!this._requests[messageId]) {
260 // Error
261 throw new Error(`Error for unknown message id ${messageId}`);
262 }
263 // eslint-disable-next-line no-case-declarations
264 let rejectCallback;
265 if (Utils.isIterable(this._requests[messageId])) {
266 [, rejectCallback] = this._requests[messageId];
267 } else {
268 throw new Error(`Error request for unknown message id ${messageId} is not iterable`);
269 }
270 delete this._requests[messageId];
271 rejectCallback(new OCPPError(commandName, commandPayload, errorDetails));
272 break;
273 // Error
274 default:
275 throw new Error(`Wrong message type ${messageType}`);
276 }
277 } catch (error) {
278 // Log
279 logger.error('%s Incoming message %j processing error %s on request content %s', this._basicFormatLog(), message, error, this._requests[messageId]);
280 // Send error
281 // await this.sendError(messageId, error);
282 }
283 }
284
027b409a
JB
285 // eslint-disable-next-line class-methods-use-this
286 async _startHeartbeat(self) {
287 if (self._heartbeatInterval && !self._heartbeatSetInterval) {
027b409a
JB
288 self._heartbeatSetInterval = setInterval(() => {
289 try {
290 const payload = {
291 currentTime: new Date().toISOString(),
292 };
293 self.sendMessage(Utils.generateUUID(), payload, Constants.OCPP_JSON_CALL_MESSAGE, 'Heartbeat');
294 } catch (error) {
295 logger.error(self._basicFormatLog() + ' Send heartbeat error: ' + error);
296 }
297 }, self._heartbeatInterval);
0bbcb3dc 298 logger.info(self._basicFormatLog() + ' Heartbeat started every ' + self._heartbeatInterval + 'ms');
027b409a
JB
299 } else {
300 logger.error(self._basicFormatLog() + ' Heartbeat interval undefined, not starting the heartbeat');
301 }
302 }
303
7dde0b73
JB
304 _reconnect(error) {
305 logger.error(this._basicFormatLog() + ' Socket: abnormally closed', error);
5c68da4d 306 // Stop heartbeat
7dde0b73
JB
307 if (this._heartbeatSetInterval) {
308 clearInterval(this._heartbeatSetInterval);
309 this._heartbeatSetInterval = null;
310 }
34dcb3b5
JB
311 // Stop the ATG if needed
312 if (Utils.convertToBoolean(this._stationInfo.AutomaticTransactionGenerator.enable) &&
313 Utils.convertToBoolean(this._stationInfo.AutomaticTransactionGenerator.stopOnConnectionFailure) &&
314 this._automaticTransactionGeneration &&
315 !this._automaticTransactionGeneration.timeToStop) {
7dde0b73
JB
316 this._automaticTransactionGeneration.stop();
317 }
318 if (this._autoReconnectTimeout !== 0 &&
319 (this._autoReconnectRetryCount < this._autoReconnectMaxRetries || this._autoReconnectMaxRetries === -1)) {
320 logger.error(`${this._basicFormatLog()} Socket: connection retry with timeout ${this._autoReconnectTimeout}ms`);
321 this._autoReconnectRetryCount++;
322 setTimeout(() => {
323 logger.error(this._basicFormatLog() + ' Socket: reconnecting try #' + this._autoReconnectRetryCount);
324 this.start();
325 }, this._autoReconnectTimeout);
326 } else if (this._autoReconnectTimeout !== 0 || this._autoReconnectMaxRetries !== -1) {
327 logger.error(`${this._basicFormatLog()} Socket: max retries reached (${this._autoReconnectRetryCount}) or retry disabled (${this._autoReconnectTimeout})`);
328 }
329 }
330
7dde0b73
JB
331 sendError(messageId, err) {
332 // Check exception: only OCPP error are accepted
72766a82 333 const error = err instanceof OCPPError ? err : new OCPPError(Constants.OCPP_ERROR_INTERNAL_ERROR, err.message);
7dde0b73 334 // Send error
f7869514 335 return this.sendMessage(messageId, error, Constants.OCPP_JSON_CALL_ERROR_MESSAGE);
7dde0b73
JB
336 }
337
027b409a
JB
338 async sendStatusNotification(connectorId, status, errorCode = 'NoError') {
339 try {
340 const payload = {
341 connectorId,
342 errorCode,
343 status,
344 };
345 await this.sendMessage(Utils.generateUUID(), payload, Constants.OCPP_JSON_CALL_MESSAGE, 'StatusNotification');
346 } catch (error) {
347 logger.error(this._basicFormatLog() + ' Send status error: ' + error);
348 }
349 }
350
351 async sendStatusNotificationWithTimeout(connectorId, status, errorCode = 'NoError', timeout = Constants.STATUS_NOTIFICATION_TIMEOUT) {
352 setTimeout(() => this.sendStatusNotification(connectorId, status, errorCode), timeout);
353 }
354
f7869514 355 sendMessage(messageId, command, messageType = Constants.OCPP_JSON_CALL_RESULT_MESSAGE, commandName = '') {
7dde0b73
JB
356 // Send a message through wsConnection
357 const self = this;
358 // Create a promise
359 return new Promise((resolve, reject) => {
360 let messageToSend;
361 // Type of message
362 switch (messageType) {
363 // Request
f7869514 364 case Constants.OCPP_JSON_CALL_MESSAGE:
7dde0b73
JB
365 this._statistics.addMessage(commandName);
366 // Build request
367 this._requests[messageId] = [responseCallback, rejectCallback, command];
368 messageToSend = JSON.stringify([messageType, messageId, commandName, command]);
369 break;
370 // Response
f7869514 371 case Constants.OCPP_JSON_CALL_RESULT_MESSAGE:
d3a7883e 372 this._statistics.addMessage(commandName);
7dde0b73
JB
373 // Build response
374 messageToSend = JSON.stringify([messageType, messageId, command]);
375 break;
376 // Error Message
f7869514 377 case Constants.OCPP_JSON_CALL_ERROR_MESSAGE:
7dde0b73 378 // Build Message
7de604f9 379 this._statistics.addMessage(`Error ${command.code ? command.code : Constants.OCPP_ERROR_GENERIC_ERROR} on ${commandName || ''}`);
894a1780 380 messageToSend = JSON.stringify([messageType, messageId, command.code ? command.code : Constants.OCPP_ERROR_GENERIC_ERROR, command.message ? command.message : '', command.details ? command.details : {}]);
7dde0b73
JB
381 break;
382 }
2e6f5966 383 // Check if wsConnection is ready
7dde0b73
JB
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?
f7869514 392 if (messageType !== Constants.OCPP_JSON_CALL_MESSAGE) {
7dde0b73
JB
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
f7869514 398 setTimeout(() => rejectCallback(`Timeout for message ${messageId}`), Constants.OCPP_SOCKET_TIMEOUT);
7dde0b73
JB
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 {
61c2e33d 408 logger.debug(self._basicFormatLog() + ' Trying to call an undefined response callback function: ' + responseCallbackFn);
7dde0b73
JB
409 }
410 // Send the response
411 resolve(payload);
412 }
413
414 // Function that will receive the request's rejection
415 function rejectCallback(reason) {
7de604f9 416 self._statistics.addMessage(`Error ${command.code ? command.code : Constants.OCPP_ERROR_GENERIC_ERROR} on ${commandName || ''}`, true);
7dde0b73
JB
417 // Build Exception
418 // eslint-disable-next-line no-empty-function
419 self._requests[messageId] = [() => { }, () => { }, '']; // Properly format the request
420 const error = reason instanceof OCPPError ? reason : new Error(reason);
421 // Send error
422 reject(error);
423 }
424 });
425 }
426
027b409a 427 async _basicStartMessageSequence() {
5c68da4d 428 // Start heartbeat
7dde0b73 429 this._startHeartbeat(this);
61c2e33d 430 // Build connectors
2e6f5966 431 if (!this._connectors) {
7dde0b73 432 this._connectors = {};
2e6f5966 433 const connectorsConfig = Utils.cloneJSonDocument(this._stationInfo.Connectors);
61c2e33d 434 // Determine number of customized connectors
7dde0b73
JB
435 let lastConnector;
436 for (lastConnector in connectorsConfig) {
61c2e33d
JB
437 // Add connector 0, OCPP specification violation that for example KEBA have
438 if (Utils.convertToInt(lastConnector) === 0 && Utils.convertToBoolean(this._stationInfo.useConnectorId0) &&
d3a7883e 439 connectorsConfig[lastConnector]) {
7dde0b73
JB
440 this._connectors[lastConnector] = connectorsConfig[lastConnector];
441 }
442 }
443 let maxConnectors = 0;
444 if (Array.isArray(this._stationInfo.numberOfConnectors)) {
61c2e33d 445 // Generate some connectors
7dde0b73
JB
446 maxConnectors = this._stationInfo.numberOfConnectors[(this._index - 1) % this._stationInfo.numberOfConnectors.length];
447 } else {
448 maxConnectors = this._stationInfo.numberOfConnectors;
449 }
61c2e33d
JB
450 this._addConfigurationKey('NumberOfConnectors', maxConnectors, true);
451 // Generate all connectors
7dde0b73 452 for (let index = 1; index <= maxConnectors; index++) {
34dcb3b5 453 const randConnectorID = Utils.convertToBoolean(this._stationInfo.randomConnectors) ? Utils.getRandomInt(lastConnector, 1) : index;
7dde0b73
JB
454 this._connectors[index] = connectorsConfig[randConnectorID];
455 }
456 }
457
458 for (const connector in this._connectors) {
459 if (!this._connectors[connector].transactionStarted) {
460 if (this._connectors[connector].bootStatus) {
027b409a 461 this.sendStatusNotificationWithTimeout(connector, this._connectors[connector].bootStatus);
7dde0b73 462 } else {
027b409a 463 this.sendStatusNotificationWithTimeout(connector, 'Available');
7dde0b73
JB
464 }
465 } else {
027b409a 466 this.sendStatusNotificationWithTimeout(connector, 'Charging');
7dde0b73
JB
467 }
468 }
469
34dcb3b5 470 if (Utils.convertToBoolean(this._stationInfo.AutomaticTransactionGenerator.enable)) {
7dde0b73
JB
471 if (!this._automaticTransactionGeneration) {
472 this._automaticTransactionGeneration = new AutomaticTransactionGenerator(this);
473 }
34dcb3b5
JB
474 if (this._automaticTransactionGeneration.timeToStop) {
475 this._automaticTransactionGeneration.start();
476 }
7dde0b73
JB
477 }
478 this._statistics.start();
479 }
480
027b409a
JB
481 _resetTransactionOnConnector(connectorID) {
482 this._connectors[connectorID].transactionStarted = false;
483 this._connectors[connectorID].transactionId = null;
d3a7883e 484 this._connectors[connectorID].idTag = null;
027b409a
JB
485 this._connectors[connectorID].lastConsumptionValue = -1;
486 this._connectors[connectorID].lastSoC = -1;
d3a7883e
JB
487 if (this._connectors[connectorID].transactionSetInterval) {
488 clearInterval(this._connectors[connectorID].transactionSetInterval);
027b409a
JB
489 }
490 }
491
492 handleResponseBootNotification(payload) {
493 if (payload.status === 'Accepted') {
494 this._heartbeatInterval = payload.interval * 1000;
5c68da4d
JB
495 this._addConfigurationKey('HeartBeatInterval', Utils.convertToInt(payload.interval));
496 this._addConfigurationKey('HeartbeatInterval', Utils.convertToInt(payload.interval), false, false);
027b409a
JB
497 this._basicStartMessageSequence();
498 } else {
499 logger.info(this._basicFormatLog() + ' Boot Notification rejected');
500 }
501 }
502
7dde0b73 503 handleResponseStartTransaction(payload, requestPayload) {
d3a7883e
JB
504 if (this._connectors[requestPayload.connectorId].transactionStarted) {
505 logger.debug(this._basicFormatLog() + ' Try to start a transaction on an already used connector ' + requestPayload.connectorId + ' by transaction ' + this._connectors[requestPayload.connectorId].transactionId);
506 }
84393381 507
7de604f9
JB
508 let transactionConnectorId;
509 for (const connector in this._connectors) {
510 if (Utils.convertToInt(connector) === Utils.convertToInt(requestPayload.connectorId)) {
511 transactionConnectorId = connector;
512 break;
7dde0b73 513 }
7de604f9
JB
514 }
515 if (!transactionConnectorId) {
516 logger.error(this._basicFormatLog() + ' Try to start a transaction on a non existing connector Id ' + requestPayload.connectorId);
517 return;
518 }
519 if (payload.idTagInfo && payload.idTagInfo.status === 'Accepted') {
520 this._connectors[transactionConnectorId].transactionStarted = true;
521 this._connectors[transactionConnectorId].transactionId = payload.transactionId;
522 this._connectors[transactionConnectorId].idTag = requestPayload.idTag;
523 this._connectors[transactionConnectorId].lastConsumptionValue = 0;
524 this._connectors[transactionConnectorId].lastSoC = 0;
525 this.sendStatusNotification(requestPayload.connectorId, 'Charging');
526 logger.info(this._basicFormatLog() + ' Transaction ' + this._connectors[transactionConnectorId].transactionId + ' STARTED on ' + this._stationInfo.name + '#' + requestPayload.connectorId + ' for idTag ' + requestPayload.idTag);
527 const configuredMeterValueSampleInterval = this._getConfigurationKey('MeterValueSampleInterval');
528 this.startMeterValues(requestPayload.connectorId,
529 configuredMeterValueSampleInterval ? configuredMeterValueSampleInterval.value * 1000 : 60000);
7dde0b73
JB
530 } else {
531 logger.error(this._basicFormatLog() + ' Starting transaction id ' + payload.transactionId + ' REJECTED with status ' + payload.idTagInfo.status + ', idTag ' + requestPayload.idTag);
7de604f9 532 this._resetTransactionOnConnector(transactionConnectorId);
7dde0b73
JB
533 this.sendStatusNotification(requestPayload.connectorId, 'Available');
534 }
535 }
536
34dcb3b5 537 handleResponseStopTransaction(payload, requestPayload) {
d3a7883e
JB
538 let transactionConnectorId;
539 for (const connector in this._connectors) {
540 if (this._connectors[connector].transactionId === requestPayload.transactionId) {
541 transactionConnectorId = connector;
542 break;
543 }
544 }
545 if (!transactionConnectorId) {
7de604f9
JB
546 logger.error(this._basicFormatLog() + ' Try to stop a non existing transaction ' + requestPayload.transactionId);
547 return;
d3a7883e
JB
548 }
549 if (payload.idTagInfo && payload.idTagInfo.status === 'Accepted') {
550 this.sendStatusNotification(transactionConnectorId, 'Available');
551 logger.info(this._basicFormatLog() + ' Transaction ' + this._connectors[transactionConnectorId].transactionId + ' STOPPED on ' + this._stationInfo.name + '#' + transactionConnectorId);
552 this._resetTransactionOnConnector(transactionConnectorId);
34dcb3b5 553 } else {
d3a7883e 554 logger.error(this._basicFormatLog() + ' Stopping transaction id ' + this._connectors[transactionConnectorId].transactionId + ' REJECTED with status ' + payload.idTagInfo.status);
34dcb3b5
JB
555 }
556 }
557
facd8ebd
JB
558 handleResponseStatusNotification(payload, requestPayload) {
559 logger.debug(this._basicFormatLog() + ' Status notification response received: %j to status notification request: %j', payload, requestPayload);
7dde0b73
JB
560 }
561
facd8ebd
JB
562 handleResponseMeterValues(payload, requestPayload) {
563 logger.debug(this._basicFormatLog() + ' MeterValues response received: %j to MeterValues request: %j', payload, requestPayload);
027b409a
JB
564 }
565
facd8ebd
JB
566 handleResponseHeartbeat(payload, requestPayload) {
567 logger.debug(this._basicFormatLog() + ' Heartbeat response received: %j to Heartbeat request: %j', payload, requestPayload);
7dde0b73
JB
568 }
569
570 async handleRequest(messageId, commandName, commandPayload) {
571 let result;
572 this._statistics.addMessage(commandName, true);
573 // Call
574 if (typeof this['handle' + commandName] === 'function') {
575 try {
576 // Call the method
577 result = await this['handle' + commandName](commandPayload);
578 } catch (error) {
579 // Log
580 logger.error(this._basicFormatLog() + ' Handle request error: ' + error);
facd8ebd 581 // Send back response to inform backend
7dde0b73
JB
582 await this.sendError(messageId, error);
583 }
584 } else {
84393381 585 // Throw exception
f7869514 586 await this.sendError(messageId, new OCPPError(Constants.OCPP_ERROR_NOT_IMPLEMENTED, 'Not implemented', {}));
7dde0b73
JB
587 throw new Error(`${commandName} is not implemented ${JSON.stringify(commandPayload, null, ' ')}`);
588 }
84393381 589 // Send response
f7869514 590 await this.sendMessage(messageId, result, Constants.OCPP_JSON_CALL_RESULT_MESSAGE);
7dde0b73
JB
591 }
592
61c2e33d
JB
593 _getConfigurationKey(key) {
594 return this._configuration.configurationKey.find((configElement) => configElement.key === key);
595 }
596
3497da01 597 _addConfigurationKey(key, value, readonly = false, visible = true, reboot = false) {
61c2e33d
JB
598 const keyFound = this._getConfigurationKey(key);
599 if (!keyFound) {
600 this._configuration.configurationKey.push({
601 key,
602 readonly,
603 value,
604 visible,
3497da01 605 reboot,
61c2e33d
JB
606 });
607 }
608 }
609
610 _setConfigurationKeyValue(key, value) {
611 const keyFound = this._getConfigurationKey(key);
612 if (keyFound) {
d3a7883e
JB
613 const keyIndex = this._configuration.configurationKey.indexOf(keyFound);
614 this._configuration.configurationKey[keyIndex].value = value;
61c2e33d
JB
615 }
616 }
617
34dcb3b5 618 async handleGetConfiguration(commandPayload) {
facd8ebd
JB
619 const configurationKey = [];
620 const unknownKey = [];
61c2e33d
JB
621 if (Utils.isEmptyArray(commandPayload.key)) {
622 for (const configuration of this._configuration.configurationKey) {
623 if (Utils.isUndefined(configuration.visible)) {
624 configuration.visible = true;
625 } else {
626 configuration.visible = Utils.convertToBoolean(configuration.visible);
627 }
628 if (!configuration.visible) {
629 continue;
630 }
631 configurationKey.push({
632 key: configuration.key,
633 readonly: configuration.readonly,
634 value: configuration.value,
635 });
facd8ebd 636 }
61c2e33d
JB
637 } else {
638 for (const configuration of commandPayload.key) {
639 const keyFound = this._getConfigurationKey(configuration);
640 if (keyFound) {
641 if (Utils.isUndefined(keyFound.visible)) {
642 keyFound.visible = true;
643 } else {
644 keyFound.visible = Utils.convertToBoolean(configuration.visible);
645 }
646 if (!keyFound.visible) {
647 continue;
648 }
649 configurationKey.push({
650 key: keyFound.key,
651 readonly: keyFound.readonly,
652 value: keyFound.value,
653 });
654 } else {
655 unknownKey.push(configuration);
656 }
facd8ebd 657 }
facd8ebd
JB
658 }
659 return {
660 configurationKey,
661 unknownKey,
662 };
7dde0b73
JB
663 }
664
665 async handleChangeConfiguration(commandPayload) {
61c2e33d 666 const keyToChange = this._getConfigurationKey(commandPayload.key);
7d887a1b
JB
667 if (!keyToChange) {
668 return {status: Constants.OCPP_ERROR_NOT_SUPPORTED};
669 } else if (keyToChange && Utils.convertToBoolean(keyToChange.readonly)) {
670 return Constants.OCPP_RESPONSE_REJECTED;
671 } else if (keyToChange && !Utils.convertToBoolean(keyToChange.readonly)) {
a6e68f34
JB
672 const keyIndex = this._configuration.configurationKey.indexOf(keyToChange);
673 this._configuration.configurationKey[keyIndex].value = commandPayload.value;
d3a7883e
JB
674 let triggerHeartbeatRestart = false;
675 if (keyToChange.key === 'HeartBeatInterval') {
676 this._setConfigurationKeyValue('HeartbeatInterval', commandPayload.value);
677 triggerHeartbeatRestart = true;
678 }
679 if (keyToChange.key === 'HeartbeatInterval') {
680 this._setConfigurationKeyValue('HeartBeatInterval', commandPayload.value);
681 triggerHeartbeatRestart = true;
682 }
683 if (triggerHeartbeatRestart) {
5c68da4d
JB
684 this._heartbeatInterval = Utils.convertToInt(commandPayload.value) * 1000;
685 // Stop heartbeat
686 if (this._heartbeatSetInterval) {
687 clearInterval(this._heartbeatSetInterval);
688 this._heartbeatSetInterval = null;
689 }
690 // Start heartbeat
691 this._startHeartbeat(this);
692 }
7d887a1b
JB
693 if (Utils.convertToBoolean(keyToChange.reboot)) {
694 return Constants.OCPP_RESPONSE_REBOOT_REQUIRED;
695 }
dcab13bd 696 return Constants.OCPP_RESPONSE_ACCEPTED;
7dde0b73 697 }
7dde0b73
JB
698 }
699
700 async handleRemoteStartTransaction(commandPayload) {
72766a82 701 const transactionConnectorID = commandPayload.connectorId ? commandPayload.connectorId : '1';
2e6f5966 702 if (this.hasAuthorizedTags() && this._getLocalAuthListEnabled() && this._getAuthorizeRemoteTxRequests()) {
dcab13bd 703 // Check if authorized
2e6f5966 704 if (this._authorizedTags.find((value) => value === commandPayload.idTag)) {
7dde0b73 705 // Authorization successful start transaction
61c2e33d 706 this.sendStartTransactionWithTimeout(transactionConnectorID, commandPayload.idTag, Constants.START_TRANSACTION_TIMEOUT);
027b409a 707 logger.debug(this._basicFormatLog() + ' Transaction remotely STARTED on ' + this._stationInfo.name + '#' + transactionConnectorID + ' for idTag ' + commandPayload.idTag);
dcab13bd 708 return Constants.OCPP_RESPONSE_ACCEPTED;
7dde0b73 709 }
84393381 710 logger.error(this._basicFormatLog() + ' Remote starting transaction REJECTED with status ' + commandPayload.idTagInfo.status + ', idTag ' + commandPayload.idTag);
dcab13bd 711 return Constants.OCPP_RESPONSE_REJECTED;
7dde0b73 712 }
dcab13bd 713 // No local authorization check required => start transaction
61c2e33d 714 this.sendStartTransactionWithTimeout(transactionConnectorID, commandPayload.idTag, Constants.START_TRANSACTION_TIMEOUT);
027b409a
JB
715 logger.debug(this._basicFormatLog() + ' Transaction remotely STARTED on ' + this._stationInfo.name + '#' + transactionConnectorID + ' for idTag ' + commandPayload.idTag);
716 return Constants.OCPP_RESPONSE_ACCEPTED;
717 }
718
719 async handleRemoteStopTransaction(commandPayload) {
720 for (const connector in this._connectors) {
721 if (this._connectors[connector].transactionId === commandPayload.transactionId) {
d3a7883e
JB
722 this.sendStopTransaction(commandPayload.transactionId);
723 return Constants.OCPP_RESPONSE_ACCEPTED;
027b409a
JB
724 }
725 }
d3a7883e
JB
726 logger.info(this._basicFormatLog() + ' Try to stop remotely a non existing transaction ' + commandPayload.transactionId);
727 return Constants.OCPP_RESPONSE_REJECTED;
7dde0b73
JB
728 }
729
730 async sendStartTransaction(connectorID, idTag) {
731 try {
732 const payload = {
733 connectorId: connectorID,
734 idTag,
735 meterStart: 0,
736 timestamp: new Date().toISOString(),
737 };
027b409a 738 return await this.sendMessage(Utils.generateUUID(), payload, Constants.OCPP_JSON_CALL_MESSAGE, 'StartTransaction');
7dde0b73
JB
739 } catch (error) {
740 logger.error(this._basicFormatLog() + ' Send start transaction error: ' + error);
7dde0b73
JB
741 throw error;
742 }
743 }
61c2e33d
JB
744 async sendStartTransactionWithTimeout(connectorID, idTag, timeout) {
745 setTimeout(() => this.sendStartTransaction(connectorID, idTag), timeout);
746 }
7dde0b73 747
d3a7883e 748 async sendStopTransaction(transactionId) {
7dde0b73
JB
749 try {
750 const payload = {
751 transactionId,
752 meterStop: 0,
753 timestamp: new Date().toISOString(),
754 };
027b409a 755 await this.sendMessage(Utils.generateUUID(), payload, Constants.OCPP_JSON_CALL_MESSAGE, 'StopTransaction');
7dde0b73
JB
756 } catch (error) {
757 logger.error(this._basicFormatLog() + ' Send stop transaction error: ' + error);
758 throw error;
7dde0b73
JB
759 }
760 }
761
7dde0b73 762 // eslint-disable-next-line class-methods-use-this
7de604f9 763 async sendMeterValues(connectorID, interval, self, debug = false) {
7dde0b73
JB
764 try {
765 const sampledValueLcl = {
766 timestamp: new Date().toISOString(),
767 };
2e6f5966 768 const meterValuesClone = Utils.cloneJSonDocument(self._getConnector(connectorID).MeterValues);
7dde0b73
JB
769 if (Array.isArray(meterValuesClone)) {
770 sampledValueLcl.sampledValue = meterValuesClone;
771 } else {
772 sampledValueLcl.sampledValue = [meterValuesClone];
773 }
774 for (let index = 0; index < sampledValueLcl.sampledValue.length; index++) {
7de604f9 775 const connector = self._connectors[connectorID];
7dde0b73 776 if (sampledValueLcl.sampledValue[index].measurand && sampledValueLcl.sampledValue[index].measurand === 'SoC') {
61c2e33d 777 sampledValueLcl.sampledValue[index].value = Utils.getRandomInt(100);
7de604f9
JB
778 if (sampledValueLcl.sampledValue[index].value > 100 || debug) {
779 logger.error(`${self._basicFormatLog()} MeterValues measurand ${sampledValueLcl.sampledValue[index].measurand ? sampledValueLcl.sampledValue[index].measurand : 'Energy.Active.Import.Register'}: connectorID ${connectorID}, transaction ${connector.transactionId}, value: ${sampledValueLcl.sampledValue[index].value}`);
7dde0b73
JB
780 }
781 } else {
782 // Persist previous value in connector
7de604f9 783 const consumption = Utils.getRandomInt(self._stationInfo.maxPower / 3600000 * interval);
7dde0b73
JB
784 if (connector && connector.lastConsumptionValue >= 0) {
785 connector.lastConsumptionValue += consumption;
786 } else {
787 connector.lastConsumptionValue = 0;
788 }
7de604f9
JB
789 const maxConsumption = self._stationInfo.maxPower * 3600 / interval;
790 logger.info(`${self._basicFormatLog()} MeterValues measurand ${sampledValueLcl.sampledValue[index].measurand ? sampledValueLcl.sampledValue[index].measurand : 'Energy.Active.Import.Register'}: connectorID ${connectorID}, transaction ${connector.transactionId}, value ${connector.lastConsumptionValue}`);
7dde0b73 791 sampledValueLcl.sampledValue[index].value = connector.lastConsumptionValue;
7de604f9
JB
792 if (sampledValueLcl.sampledValue[index].value > maxConsumption || debug) {
793 logger.error(`${self._basicFormatLog()} MeterValues measurand ${sampledValueLcl.sampledValue[index].measurand ? sampledValueLcl.sampledValue[index].measurand : 'Energy.Active.Import.Register'}: connectorID ${connectorID}, transaction ${connector.transactionId}, value: ${sampledValueLcl.sampledValue[index].value}/${maxConsumption}`);
7dde0b73
JB
794 }
795 }
796 }
797
798 const payload = {
799 connectorId: connectorID,
800 transactionId: self._connectors[connectorID].transactionId,
801 meterValue: [sampledValueLcl],
802 };
027b409a 803 await self.sendMessage(Utils.generateUUID(), payload, Constants.OCPP_JSON_CALL_MESSAGE, 'MeterValues');
7dde0b73 804 } catch (error) {
34dcb3b5 805 logger.error(self._basicFormatLog() + ' Send MeterValues error: ' + error);
7dde0b73
JB
806 }
807 }
808
7de604f9 809 async startMeterValues(connectorID, interval) {
84393381 810 if (!this._connectors[connectorID].transactionStarted) {
7de604f9
JB
811 logger.error(`${this._basicFormatLog()} Trying to start MeterValues on connector ID ${connectorID} with no transaction started`);
812 return;
84393381 813 } else if (this._connectors[connectorID].transactionStarted && !this._connectors[connectorID].transactionId) {
7de604f9
JB
814 logger.error(`${this._basicFormatLog()} Trying to start MeterValues on connector ID ${connectorID} with no transaction id`);
815 return;
84393381 816 }
d3a7883e 817 this._connectors[connectorID].transactionSetInterval = setInterval(async () => {
7dde0b73
JB
818 const sendMeterValues = performance.timerify(this.sendMeterValues);
819 this._performanceObserver.observe({
820 entryTypes: ['function'],
821 });
7de604f9 822 await sendMeterValues(connectorID, interval, this);
7dde0b73
JB
823 }, interval);
824 }
825
2e6f5966 826 hasAuthorizedTags() {
4a56deef 827 return !Utils.isEmptyArray(this._authorizedTags);
7dde0b73
JB
828 }
829
830 getRandomTagId() {
61c2e33d 831 const index = Math.floor(Math.random() * this._authorizedTags.length);
2e6f5966 832 return this._authorizedTags[index];
7dde0b73
JB
833 }
834
835 _getConnector(number) {
836 return this._stationInfo.Connectors[number];
837 }
838}
839
840module.exports = ChargingStation;