Move attributes init to the right place.
[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');
de1f5008 10const crypto = require('crypto');
7dde0b73
JB
11const {performance, PerformanceObserver} = require('perf_hooks');
12
13class ChargingStation {
2e6f5966
JB
14 constructor(index, stationTemplateFile) {
15 this._index = index;
16 this._stationTemplateFile = stationTemplateFile;
fa9bcef2 17 this._connectors = {};
2e6f5966
JB
18 this._initialize();
19
0a60c33c 20 this._isSocketRestart = false;
7dde0b73
JB
21 this._autoReconnectRetryCount = 0;
22 this._autoReconnectMaxRetries = Configuration.getAutoReconnectMaxRetries(); // -1 for unlimited
23 this._autoReconnectTimeout = Configuration.getAutoReconnectTimeout() * 1000; // ms, zero for disabling
2e6f5966
JB
24
25 this._requests = {};
26 this._messageQueue = [];
27
83045896 28 this._authorizedTags = this._loadAndGetAuthorizedTags();
2e6f5966
JB
29 }
30
5ad8570f
JB
31 _getStationName(stationTemplate) {
32 return stationTemplate.fixedName ? stationTemplate.baseName : stationTemplate.baseName + '-' + ('000000000' + this._index).substr(('000000000' + this._index).length - 4);
33 }
34
35 _buildStationInfo() {
36 let stationTemplateFromFile;
37 try {
38 // Load template file
39 const fileDescriptor = fs.openSync(this._stationTemplateFile, 'r');
40 stationTemplateFromFile = JSON.parse(fs.readFileSync(fileDescriptor, 'utf8'));
41 fs.closeSync(fileDescriptor);
42 } catch (error) {
ead548f2 43 logger.error(this._logPrefix() + ' Template file loading error: ' + error);
5ad8570f
JB
44 }
45 const stationTemplate = stationTemplateFromFile || {};
0a60c33c 46 if (!Utils.isEmptyArray(stationTemplateFromFile.power)) {
5ad8570f
JB
47 stationTemplate.maxPower = stationTemplateFromFile.power[Math.floor(Math.random() * stationTemplateFromFile.power.length)];
48 } else {
49 stationTemplate.maxPower = stationTemplateFromFile.power;
50 }
51 stationTemplate.name = this._getStationName(stationTemplateFromFile);
0a60c33c 52 stationTemplate.resetTime = stationTemplateFromFile.resetTime ? stationTemplateFromFile.resetTime * 1000 : Constants.CHARGING_STATION_DEFAULT_RESET_TIME;
5ad8570f
JB
53 return stationTemplate;
54 }
55
2e6f5966
JB
56 _initialize() {
57 this._stationInfo = this._buildStationInfo();
58 this._bootNotificationMessage = {
59 chargePointModel: this._stationInfo.chargePointModel,
60 chargePointVendor: this._stationInfo.chargePointVendor,
6958152c 61 chargePointSerialNumber: this._stationInfo.chargePointSerialNumberPrefix ? this._stationInfo.chargePointSerialNumberPrefix : '',
34dcb3b5 62 firmwareVersion: this._stationInfo.firmwareVersion ? this._stationInfo.firmwareVersion : '',
2e6f5966
JB
63 };
64 this._configuration = this._getConfiguration();
2e6f5966 65 this._supervisionUrl = this._getSupervisionURL();
0a60c33c
JB
66 this._wsConnectionUrl = this._supervisionUrl + '/' + this._stationInfo.name;
67 // Build connectors if needed
68 const maxConnectors = this._getMaxConnectors();
69 const connectorsConfig = Utils.cloneJSonDocument(this._stationInfo.Connectors);
de1f5008
JB
70 const connectorsConfigHash = crypto.createHash('sha256').update(JSON.stringify(connectorsConfig) + maxConnectors.toString()).digest('hex');
71 // FIXME: Handle shrinking the number of connectors
72 if (!this._connectors || (this._connectors && this._connectorsConfigurationHash !== connectorsConfigHash)) {
73 this._connectorsConfigurationHash = connectorsConfigHash;
0a60c33c
JB
74 // Determine number of customized connectors
75 let lastConnector;
76 for (lastConnector in connectorsConfig) {
77 // Add connector 0, OCPP specification violation that for example KEBA have
78 if (Utils.convertToInt(lastConnector) === 0 && Utils.convertToBoolean(this._stationInfo.useConnectorId0) &&
79 connectorsConfig[lastConnector]) {
80 this._connectors[lastConnector] = connectorsConfig[lastConnector];
81 }
82 }
83 this._addConfigurationKey('NumberOfConnectors', maxConnectors, true);
84 // Generate all connectors
85 for (let index = 1; index <= maxConnectors; index++) {
86 const randConnectorID = Utils.convertToBoolean(this._stationInfo.randomConnectors) ? Utils.getRandomInt(lastConnector, 1) : index;
87 this._connectors[index] = connectorsConfig[randConnectorID];
88 }
89 }
90 // Initialize transaction attributes on connectors
91 for (const connector in this._connectors) {
92 if (!this._connectors[connector].transactionStarted) {
93 this._initTransactionOnConnector(connector);
94 }
95 }
13b4eba0 96 // FIXME: Conditionally initialize or use singleton design pattern per charging station
7dde0b73
JB
97 this._statistics = new Statistics(this._stationInfo.name);
98 this._performanceObserver = new PerformanceObserver((list) => {
99 const entry = list.getEntries()[0];
100 this._statistics.logPerformance(entry, 'ChargingStation');
101 this._performanceObserver.disconnect();
102 });
7dde0b73
JB
103 }
104
ead548f2
JB
105 _logPrefix() {
106 return Utils.logPrefix(` ${this._stationInfo.name}:`);
7dde0b73
JB
107 }
108
2e6f5966
JB
109 _getConfiguration() {
110 return this._stationInfo.Configuration ? this._stationInfo.Configuration : {};
7dde0b73
JB
111 }
112
2e6f5966
JB
113 _getAuthorizationFile() {
114 return this._stationInfo.authorizationFile ? this._stationInfo.authorizationFile : '';
7dde0b73
JB
115 }
116
83045896 117 _loadAndGetAuthorizedTags() {
2e6f5966
JB
118 let authorizedTags = [];
119 const authorizationFile = this._getAuthorizationFile();
120 if (authorizationFile) {
121 try {
122 // Load authorization file
123 const fileDescriptor = fs.openSync(authorizationFile, 'r');
124 authorizedTags = JSON.parse(fs.readFileSync(fileDescriptor, 'utf8'));
125 fs.closeSync(fileDescriptor);
126 } catch (error) {
ead548f2 127 logger.error(this._logPrefix() + ' Authorization file loading error: ' + error);
2e6f5966
JB
128 }
129 } else {
ead548f2 130 logger.info(this._logPrefix() + ' No authorization file given in template file ' + this._stationTemplateFile);
2e6f5966
JB
131 }
132 return authorizedTags;
133 }
134
5ad8570f
JB
135 getRandomTagId() {
136 const index = Math.floor(Math.random() * this._authorizedTags.length);
137 return this._authorizedTags[index];
2e6f5966
JB
138 }
139
5ad8570f
JB
140 hasAuthorizedTags() {
141 return !Utils.isEmptyArray(this._authorizedTags);
142 }
143
144 _getConnector(number) {
145 return this._stationInfo.Connectors[number];
146 }
147
148 _getMaxConnectors() {
149 let maxConnectors = 0;
0a60c33c
JB
150 if (!Utils.isEmptyArray(this._stationInfo.numberOfConnectors)) {
151 // Get evenly the number of connectors
5ad8570f
JB
152 maxConnectors = this._stationInfo.numberOfConnectors[(this._index - 1) % this._stationInfo.numberOfConnectors.length];
153 } else {
154 maxConnectors = this._stationInfo.numberOfConnectors;
155 }
156 return maxConnectors;
2e6f5966
JB
157 }
158
159 _getSupervisionURL() {
160 const supervisionUrls = Utils.cloneJSonDocument(this._stationInfo.supervisionURL ? this._stationInfo.supervisionURL : Configuration.getSupervisionURLs());
7dde0b73 161 let indexUrl = 0;
0a60c33c 162 if (!Utils.isEmptyArray(supervisionUrls)) {
2e6f5966
JB
163 if (Configuration.getDistributeStationToTenantEqually()) {
164 indexUrl = this._index % supervisionUrls.length;
7dde0b73
JB
165 } else {
166 // Get a random url
167 indexUrl = Math.floor(Math.random() * supervisionUrls.length);
168 }
169 return supervisionUrls[indexUrl];
170 }
171 return supervisionUrls;
172 }
173
7dde0b73 174 _getAuthorizeRemoteTxRequests() {
61c2e33d 175 const authorizeRemoteTxRequests = this._getConfigurationKey('AuthorizeRemoteTxRequests');
a6e68f34 176 return authorizeRemoteTxRequests ? Utils.convertToBoolean(authorizeRemoteTxRequests.value) : false;
7dde0b73
JB
177 }
178
def3d48e 179 _getLocalAuthListEnabled() {
61c2e33d 180 const localAuthListEnabled = this._getConfigurationKey('LocalAuthListEnabled');
def3d48e
JB
181 return localAuthListEnabled ? Utils.convertToBoolean(localAuthListEnabled.value) : false;
182 }
183
5ad8570f
JB
184 async _basicStartMessageSequence() {
185 // Start heartbeat
186 this._startHeartbeat(this);
0a60c33c 187 // Initialize connectors status
5ad8570f
JB
188 for (const connector in this._connectors) {
189 if (!this._connectors[connector].transactionStarted) {
190 if (this._connectors[connector].bootStatus) {
191 this.sendStatusNotificationWithTimeout(connector, this._connectors[connector].bootStatus);
192 } else {
193 this.sendStatusNotificationWithTimeout(connector, 'Available');
194 }
195 } else {
196 this.sendStatusNotificationWithTimeout(connector, 'Charging');
197 }
198 }
0a60c33c 199 // Start the ATG
5ad8570f
JB
200 if (Utils.convertToBoolean(this._stationInfo.AutomaticTransactionGenerator.enable)) {
201 if (!this._automaticTransactionGeneration) {
202 this._automaticTransactionGeneration = new AutomaticTransactionGenerator(this);
203 }
204 if (this._automaticTransactionGeneration.timeToStop) {
205 this._automaticTransactionGeneration.start();
206 }
207 }
208 this._statistics.start();
209 }
210
211 // eslint-disable-next-line class-methods-use-this
212 async _startHeartbeat(self) {
0a60c33c 213 if (self._heartbeatInterval && self._heartbeatInterval > 0 && !self._heartbeatSetInterval) {
5ad8570f 214 self._heartbeatSetInterval = setInterval(() => {
0a60c33c 215 this.sendHeartbeat();
5ad8570f 216 }, self._heartbeatInterval);
ead548f2 217 logger.info(self._logPrefix() + ' Heartbeat started every ' + self._heartbeatInterval + 'ms');
7dde0b73 218 } else {
ead548f2 219 logger.error(`${self._logPrefix()} Heartbeat interval set to ${self._heartbeatInterval}, not starting the heartbeat`);
0a60c33c
JB
220 }
221 }
222
223 async _stopHeartbeat() {
224 if (this._heartbeatSetInterval) {
225 clearInterval(this._heartbeatSetInterval);
226 this._heartbeatSetInterval = null;
7dde0b73 227 }
5ad8570f
JB
228 }
229
230 _startAuthorizationFileMonitoring() {
231 // eslint-disable-next-line no-unused-vars
232 fs.watchFile(this._getAuthorizationFile(), (current, previous) => {
233 try {
ead548f2 234 logger.debug(this._logPrefix() + ' Authorization file ' + this._getAuthorizationFile() + ' have changed, reload');
5ad8570f
JB
235 // Initialize _authorizedTags
236 this._authorizedTags = this._loadAndGetAuthorizedTags();
237 } catch (error) {
ead548f2 238 logger.error(this._logPrefix() + ' Authorization file monitoring error: ' + error);
5ad8570f
JB
239 }
240 });
241 }
242
243 _startStationTemplateFileMonitoring() {
244 // eslint-disable-next-line no-unused-vars
245 fs.watchFile(this._stationTemplateFile, (current, previous) => {
246 try {
ead548f2 247 logger.debug(this._logPrefix() + ' Template file ' + this._stationTemplateFile + ' have changed, reload');
5ad8570f
JB
248 // Initialize
249 this._initialize();
250 this._addConfigurationKey('HeartBeatInterval', Utils.convertToInt(this._heartbeatInterval ? this._heartbeatInterval / 1000 : 0));
251 this._addConfigurationKey('HeartbeatInterval', Utils.convertToInt(this._heartbeatInterval ? this._heartbeatInterval / 1000 : 0), false, false);
5ad8570f 252 } catch (error) {
ead548f2 253 logger.error(this._logPrefix() + ' Charging station template file monitoring error: ' + error);
5ad8570f
JB
254 }
255 });
256 }
257
bec64e8b
JB
258 async _startMeterValues(connectorId, interval) {
259 if (!this._connectors[connectorId].transactionStarted) {
ead548f2 260 logger.error(`${this._logPrefix()} Trying to start MeterValues on connector ID ${connectorId} with no transaction started`);
5ad8570f 261 return;
bec64e8b 262 } else if (this._connectors[connectorId].transactionStarted && !this._connectors[connectorId].transactionId) {
ead548f2 263 logger.error(`${this._logPrefix()} Trying to start MeterValues on connector ID ${connectorId} with no transaction id`);
5ad8570f
JB
264 return;
265 }
0a60c33c 266 if (interval > 0) {
bec64e8b 267 this._connectors[connectorId].transactionSetInterval = setInterval(async () => {
0a60c33c
JB
268 const sendMeterValues = performance.timerify(this.sendMeterValues);
269 this._performanceObserver.observe({
270 entryTypes: ['function'],
271 });
bec64e8b 272 await sendMeterValues(connectorId, interval, this);
0a60c33c
JB
273 }, interval);
274 } else {
ead548f2 275 logger.error(`${this._logPrefix()} Charging station MeterValueSampleInterval configuration set to ${interval}ms, not sending MeterValues`);
0a60c33c 276 }
7dde0b73
JB
277 }
278
279 async start() {
0a60c33c
JB
280 if (!this._wsConnectionUrl) {
281 this._wsConnectionUrl = this._supervisionUrl + '/' + this._stationInfo.name;
5ad8570f 282 }
0a60c33c 283 this._wsConnection = new WebSocket(this._wsConnectionUrl, 'ocpp' + Constants.OCPP_VERSION_16);
ead548f2 284 logger.info(this._logPrefix() + ' Will communicate through URL ' + this._supervisionUrl);
2e6f5966
JB
285 // Monitor authorization file
286 this._startAuthorizationFileMonitoring();
287 // Monitor station template file
288 this._startStationTemplateFileMonitoring();
7dde0b73
JB
289 // Handle Socket incoming messages
290 this._wsConnection.on('message', this.onMessage.bind(this));
291 // Handle Socket error
292 this._wsConnection.on('error', this.onError.bind(this));
293 // Handle Socket close
294 this._wsConnection.on('close', this.onClose.bind(this));
295 // Handle Socket opening connection
296 this._wsConnection.on('open', this.onOpen.bind(this));
297 // Handle Socket ping
298 this._wsConnection.on('ping', this.onPing.bind(this));
299 }
300
2d0e26f5 301 async stop(reason = '') {
5ad8570f 302 // Stop heartbeat
0a60c33c 303 await this._stopHeartbeat();
5ad8570f
JB
304 // Stop the ATG
305 if (Utils.convertToBoolean(this._stationInfo.AutomaticTransactionGenerator.enable) &&
306 this._automaticTransactionGeneration &&
307 !this._automaticTransactionGeneration.timeToStop) {
2d0e26f5 308 await this._automaticTransactionGeneration.stop(reason);
5ad8570f
JB
309 } else {
310 for (const connector in this._connectors) {
311 if (this._connectors[connector].transactionStarted) {
2d0e26f5 312 await this.sendStopTransaction(this._connectors[connector].transactionId, reason);
5ad8570f
JB
313 }
314 }
315 }
316 // eslint-disable-next-line guard-for-in
317 for (const connector in this._connectors) {
318 await this.sendStatusNotification(connector, 'Unavailable');
319 }
0a60c33c 320 if (this._wsConnection && this._wsConnection.readyState === WebSocket.OPEN) {
5ad8570f
JB
321 await this._wsConnection.close();
322 }
323 }
324
325 _reconnect(error) {
ead548f2 326 logger.error(this._logPrefix() + ' Socket: abnormally closed', error);
5ad8570f
JB
327 // Stop the ATG if needed
328 if (Utils.convertToBoolean(this._stationInfo.AutomaticTransactionGenerator.enable) &&
329 Utils.convertToBoolean(this._stationInfo.AutomaticTransactionGenerator.stopOnConnectionFailure) &&
330 this._automaticTransactionGeneration &&
331 !this._automaticTransactionGeneration.timeToStop) {
332 this._automaticTransactionGeneration.stop();
333 }
334 // Stop heartbeat
0a60c33c 335 this._stopHeartbeat();
5ad8570f
JB
336 if (this._autoReconnectTimeout !== 0 &&
337 (this._autoReconnectRetryCount < this._autoReconnectMaxRetries || this._autoReconnectMaxRetries === -1)) {
ead548f2 338 logger.error(`${this._logPrefix()} Socket: connection retry with timeout ${this._autoReconnectTimeout}ms`);
5ad8570f
JB
339 this._autoReconnectRetryCount++;
340 setTimeout(() => {
ead548f2 341 logger.error(this._logPrefix() + ' Socket: reconnecting try #' + this._autoReconnectRetryCount);
5ad8570f
JB
342 this.start();
343 }, this._autoReconnectTimeout);
344 } else if (this._autoReconnectTimeout !== 0 || this._autoReconnectMaxRetries !== -1) {
ead548f2 345 logger.error(`${this._logPrefix()} Socket: max retries reached (${this._autoReconnectRetryCount}) or retry disabled (${this._autoReconnectTimeout})`);
5ad8570f
JB
346 }
347 }
348
7dde0b73 349 onOpen() {
ead548f2 350 logger.info(`${this._logPrefix()} Is connected to server through ${this._wsConnectionUrl}`);
5ad8570f 351 if (!this._isSocketRestart) {
0bbcb3dc 352 // Send BootNotification
0a60c33c 353 this.sendBootNotification();
0bbcb3dc 354 }
7dde0b73 355 if (this._isSocketRestart) {
027b409a 356 this._basicStartMessageSequence();
546dec0f 357 if (!Utils.isEmptyArray(this._messageQueue)) {
7dde0b73 358 this._messageQueue.forEach((message) => {
0a60c33c 359 if (this._wsConnection && this._wsConnection.readyState === WebSocket.OPEN) {
7dde0b73
JB
360 this._wsConnection.send(message);
361 }
362 });
363 }
7dde0b73
JB
364 }
365 this._autoReconnectRetryCount = 0;
366 this._isSocketRestart = false;
367 }
368
369 onError(error) {
370 switch (error) {
371 case 'ECONNREFUSED':
372 this._isSocketRestart = true;
373 this._reconnect(error);
374 break;
375 default:
ead548f2 376 logger.error(this._logPrefix() + ' Socket error: ' + error);
7dde0b73
JB
377 break;
378 }
379 }
380
381 onClose(error) {
382 switch (error) {
383 case 1000: // Normal close
384 case 1005:
ead548f2 385 logger.info(this._logPrefix() + ' Socket normally closed ' + error);
7dde0b73
JB
386 this._autoReconnectRetryCount = 0;
387 break;
388 default: // Abnormal close
389 this._isSocketRestart = true;
390 this._reconnect(error);
391 break;
392 }
393 }
394
395 onPing() {
ead548f2 396 logger.debug(this._logPrefix() + ' Has received a WS ping (rfc6455) from the server');
7dde0b73
JB
397 }
398
399 async onMessage(message) {
2d8cee5a 400 let [messageType, messageId, commandName, commandPayload, errorDetails] = [0, '', Constants.ENTITY_CHARGING_STATION, '', ''];
7dde0b73 401 try {
2d8cee5a
JB
402 // Parse the message
403 [messageType, messageId, commandName, commandPayload, errorDetails] = JSON.parse(message);
404
7dde0b73
JB
405 // Check the Type of message
406 switch (messageType) {
407 // Incoming Message
f7869514 408 case Constants.OCPP_JSON_CALL_MESSAGE:
7dde0b73 409 // Process the call
7dde0b73
JB
410 await this.handleRequest(messageId, commandName, commandPayload);
411 break;
412 // Outcome Message
f7869514 413 case Constants.OCPP_JSON_CALL_RESULT_MESSAGE:
7dde0b73
JB
414 // Respond
415 // eslint-disable-next-line no-case-declarations
416 let responseCallback; let requestPayload;
417 if (Utils.isIterable(this._requests[messageId])) {
418 [responseCallback, , requestPayload] = this._requests[messageId];
419 } else {
420 throw new Error(`Response request for unknown message id ${messageId} is not iterable`);
421 }
422 if (!responseCallback) {
423 // Error
424 throw new Error(`Response for unknown message id ${messageId}`);
425 }
426 delete this._requests[messageId];
7dde0b73
JB
427 responseCallback(commandName, requestPayload);
428 break;
429 // Error Message
f7869514 430 case Constants.OCPP_JSON_CALL_ERROR_MESSAGE:
7dde0b73
JB
431 if (!this._requests[messageId]) {
432 // Error
433 throw new Error(`Error for unknown message id ${messageId}`);
434 }
435 // eslint-disable-next-line no-case-declarations
436 let rejectCallback;
437 if (Utils.isIterable(this._requests[messageId])) {
438 [, rejectCallback] = this._requests[messageId];
439 } else {
440 throw new Error(`Error request for unknown message id ${messageId} is not iterable`);
441 }
442 delete this._requests[messageId];
443 rejectCallback(new OCPPError(commandName, commandPayload, errorDetails));
444 break;
445 // Error
446 default:
447 throw new Error(`Wrong message type ${messageType}`);
448 }
449 } catch (error) {
450 // Log
ead548f2 451 logger.error('%s Incoming message %j processing error %s on request content %s', this._logPrefix(), message, error, this._requests[messageId]);
7dde0b73
JB
452 // Send error
453 // await this.sendError(messageId, error);
454 }
455 }
456
0a60c33c
JB
457 sendHeartbeat() {
458 try {
459 const payload = {
460 currentTime: new Date().toISOString(),
461 };
462 this.sendMessage(Utils.generateUUID(), payload, Constants.OCPP_JSON_CALL_MESSAGE, 'Heartbeat');
463 } catch (error) {
ead548f2 464 logger.error(this._logPrefix() + ' Send Heartbeat error: ' + error);
0a60c33c
JB
465 throw error;
466 }
467 }
468
469 sendBootNotification() {
470 try {
471 this.sendMessage(Utils.generateUUID(), this._bootNotificationMessage, Constants.OCPP_JSON_CALL_MESSAGE, 'BootNotification');
472 } catch (error) {
ead548f2 473 logger.error(this._logPrefix() + ' Send BootNotification error: ' + error);
0a60c33c
JB
474 throw error;
475 }
476 }
477
5ad8570f
JB
478 async sendStatusNotification(connectorId, status, errorCode = 'NoError') {
479 try {
480 const payload = {
481 connectorId,
482 errorCode,
483 status,
484 };
485 await this.sendMessage(Utils.generateUUID(), payload, Constants.OCPP_JSON_CALL_MESSAGE, 'StatusNotification');
486 } catch (error) {
ead548f2 487 logger.error(this._logPrefix() + ' Send StatusNotification error: ' + error);
5ad8570f 488 throw error;
027b409a
JB
489 }
490 }
491
5ad8570f
JB
492 sendStatusNotificationWithTimeout(connectorId, status, errorCode = 'NoError', timeout = Constants.STATUS_NOTIFICATION_TIMEOUT) {
493 setTimeout(() => this.sendStatusNotification(connectorId, status, errorCode), timeout);
494 }
495
bec64e8b 496 async sendStartTransaction(connectorId, idTag) {
5ad8570f
JB
497 try {
498 const payload = {
bec64e8b 499 connectorId,
5ad8570f
JB
500 idTag,
501 meterStart: 0,
502 timestamp: new Date().toISOString(),
503 };
504 return await this.sendMessage(Utils.generateUUID(), payload, Constants.OCPP_JSON_CALL_MESSAGE, 'StartTransaction');
505 } catch (error) {
ead548f2 506 logger.error(this._logPrefix() + ' Send StartTransaction error: ' + error);
5ad8570f 507 throw error;
7dde0b73
JB
508 }
509 }
510
bec64e8b
JB
511 sendStartTransactionWithTimeout(connectorId, idTag, timeout) {
512 setTimeout(() => this.sendStartTransaction(connectorId, idTag), timeout);
7dde0b73
JB
513 }
514
5ad8570f 515 async sendStopTransaction(transactionId, reason = '') {
027b409a 516 try {
bec64e8b 517 let payload;
2d0e26f5
JB
518 if (reason) {
519 payload = {
520 transactionId,
521 meterStop: 0,
522 timestamp: new Date().toISOString(),
523 reason,
524 };
525 } else {
526 payload = {
527 transactionId,
528 meterStop: 0,
529 timestamp: new Date().toISOString(),
530 };
531 }
5ad8570f 532 await this.sendMessage(Utils.generateUUID(), payload, Constants.OCPP_JSON_CALL_MESSAGE, 'StopTransaction');
027b409a 533 } catch (error) {
ead548f2 534 logger.error(this._logPrefix() + ' Send StopTransaction error: ' + error);
5ad8570f 535 throw error;
027b409a
JB
536 }
537 }
538
5ad8570f 539 // eslint-disable-next-line class-methods-use-this
bec64e8b 540 async sendMeterValues(connectorId, interval, self, debug = false) {
5ad8570f
JB
541 try {
542 const sampledValueLcl = {
543 timestamp: new Date().toISOString(),
544 };
bec64e8b 545 const meterValuesClone = Utils.cloneJSonDocument(self._getConnector(connectorId).MeterValues);
0a60c33c 546 if (!Utils.isEmptyArray(meterValuesClone)) {
5ad8570f
JB
547 sampledValueLcl.sampledValue = meterValuesClone;
548 } else {
549 sampledValueLcl.sampledValue = [meterValuesClone];
550 }
551 for (let index = 0; index < sampledValueLcl.sampledValue.length; index++) {
bec64e8b 552 const connector = self._connectors[connectorId];
0a60c33c 553 // SoC measurand
5ad8570f 554 if (sampledValueLcl.sampledValue[index].measurand && sampledValueLcl.sampledValue[index].measurand === 'SoC') {
5c389abe 555 sampledValueLcl.sampledValue[index].value = !Utils.isUndefined(sampledValueLcl.sampledValue[index].value) ?
9b25a525
JB
556 sampledValueLcl.sampledValue[index].value :
557 sampledValueLcl.sampledValue[index].value = Utils.getRandomInt(100);
5ad8570f 558 if (sampledValueLcl.sampledValue[index].value > 100 || debug) {
ead548f2 559 logger.error(`${self._logPrefix()} MeterValues measurand ${sampledValueLcl.sampledValue[index].measurand ? sampledValueLcl.sampledValue[index].measurand : 'Energy.Active.Import.Register'}: connectorId ${connectorId}, transaction ${connector.transactionId}, value: ${sampledValueLcl.sampledValue[index].value}`);
5ad8570f 560 }
0a60c33c
JB
561 // Voltage measurand
562 } else if (sampledValueLcl.sampledValue[index].measurand && sampledValueLcl.sampledValue[index].measurand === 'Voltage') {
5c389abe 563 sampledValueLcl.sampledValue[index].value = !Utils.isUndefined(sampledValueLcl.sampledValue[index].value) ? sampledValueLcl.sampledValue[index].value : 230;
0a60c33c
JB
564 // Energy.Active.Import.Register measurand (default)
565 } else if (!sampledValueLcl.sampledValue[index].measurand || sampledValueLcl.sampledValue[index].measurand === 'Energy.Active.Import.Register') {
5c389abe 566 if (Utils.isUndefined(sampledValueLcl.sampledValue[index].value)) {
9b25a525
JB
567 const measurandValue = Utils.getRandomInt(self._stationInfo.maxPower / 3600000 * interval);
568 // Persist previous value in connector
569 if (connector && connector.lastEnergyActiveImportRegisterValue >= 0) {
570 connector.lastEnergyActiveImportRegisterValue += measurandValue;
571 } else {
572 connector.lastEnergyActiveImportRegisterValue = 0;
573 }
574 sampledValueLcl.sampledValue[index].value = connector.lastEnergyActiveImportRegisterValue;
5ad8570f 575 }
ead548f2 576 logger.info(`${self._logPrefix()} MeterValues measurand ${sampledValueLcl.sampledValue[index].measurand ? sampledValueLcl.sampledValue[index].measurand : 'Energy.Active.Import.Register'}: connectorId ${connectorId}, transaction ${connector.transactionId}, value ${sampledValueLcl.sampledValue[index].value}`);
5ad8570f 577 const maxConsumption = self._stationInfo.maxPower * 3600 / interval;
5ad8570f 578 if (sampledValueLcl.sampledValue[index].value > maxConsumption || debug) {
ead548f2 579 logger.error(`${self._logPrefix()} 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}`);
5ad8570f 580 }
0a60c33c
JB
581 // Unsupported measurand
582 } else {
ead548f2 583 logger.info(`${self._logPrefix()} Unsupported MeterValues measurand ${sampledValueLcl.sampledValue[index].measurand ? sampledValueLcl.sampledValue[index].measurand : 'Energy.Active.Import.Register'} on connectorId ${connectorId}`);
5ad8570f
JB
584 }
585 }
586
587 const payload = {
bec64e8b
JB
588 connectorId,
589 transactionId: self._connectors[connectorId].transactionId,
5ad8570f
JB
590 meterValue: [sampledValueLcl],
591 };
592 await self.sendMessage(Utils.generateUUID(), payload, Constants.OCPP_JSON_CALL_MESSAGE, 'MeterValues');
593 } catch (error) {
ead548f2 594 logger.error(self._logPrefix() + ' Send MeterValues error: ' + error);
0a60c33c 595 throw error;
5ad8570f
JB
596 }
597 }
598
599 sendError(messageId, err) {
600 // Check exception: only OCPP error are accepted
601 const error = err instanceof OCPPError ? err : new OCPPError(Constants.OCPP_ERROR_INTERNAL_ERROR, err.message);
602 // Send error
603 return this.sendMessage(messageId, error, Constants.OCPP_JSON_CALL_ERROR_MESSAGE);
027b409a
JB
604 }
605
f7869514 606 sendMessage(messageId, command, messageType = Constants.OCPP_JSON_CALL_RESULT_MESSAGE, commandName = '') {
7dde0b73
JB
607 // Send a message through wsConnection
608 const self = this;
609 // Create a promise
610 return new Promise((resolve, reject) => {
611 let messageToSend;
612 // Type of message
613 switch (messageType) {
614 // Request
f7869514 615 case Constants.OCPP_JSON_CALL_MESSAGE:
7dde0b73
JB
616 this._statistics.addMessage(commandName);
617 // Build request
618 this._requests[messageId] = [responseCallback, rejectCallback, command];
619 messageToSend = JSON.stringify([messageType, messageId, commandName, command]);
620 break;
621 // Response
f7869514 622 case Constants.OCPP_JSON_CALL_RESULT_MESSAGE:
d3a7883e 623 this._statistics.addMessage(commandName);
7dde0b73
JB
624 // Build response
625 messageToSend = JSON.stringify([messageType, messageId, command]);
626 break;
627 // Error Message
f7869514 628 case Constants.OCPP_JSON_CALL_ERROR_MESSAGE:
7dde0b73 629 // Build Message
52f96caf 630 this._statistics.addMessage(`Error ${command.code ? command.code : Constants.OCPP_ERROR_GENERIC_ERROR} on ${commandName}`);
894a1780 631 messageToSend = JSON.stringify([messageType, messageId, command.code ? command.code : Constants.OCPP_ERROR_GENERIC_ERROR, command.message ? command.message : '', command.details ? command.details : {}]);
7dde0b73
JB
632 break;
633 }
2e6f5966 634 // Check if wsConnection is ready
0a60c33c 635 if (this._wsConnection && this._wsConnection.readyState === WebSocket.OPEN) {
7dde0b73
JB
636 // Yes: Send Message
637 this._wsConnection.send(messageToSend);
638 } else {
639 // Buffer message until connection is back
640 this._messageQueue.push(messageToSend);
641 }
642 // Request?
f7869514 643 if (messageType !== Constants.OCPP_JSON_CALL_MESSAGE) {
7dde0b73
JB
644 // Yes: send Ok
645 resolve();
0a60c33c 646 } else if (this._wsConnection && this._wsConnection.readyState === WebSocket.OPEN) {
7dde0b73
JB
647 // Send timeout in case connection is open otherwise wait for ever
648 // FIXME: Handle message on timeout
f7869514 649 setTimeout(() => rejectCallback(`Timeout for message ${messageId}`), Constants.OCPP_SOCKET_TIMEOUT);
7dde0b73
JB
650 }
651
652 // Function that will receive the request's response
653 function responseCallback(payload, requestPayload) {
654 self._statistics.addMessage(commandName, true);
655 const responseCallbackFn = 'handleResponse' + commandName;
656 if (typeof self[responseCallbackFn] === 'function') {
657 self[responseCallbackFn](payload, requestPayload, self);
658 } else {
ead548f2 659 logger.debug(self._logPrefix() + ' Trying to call an undefined response callback function: ' + responseCallbackFn);
7dde0b73
JB
660 }
661 // Send the response
662 resolve(payload);
663 }
664
665 // Function that will receive the request's rejection
666 function rejectCallback(reason) {
52f96caf 667 self._statistics.addMessage(`Error ${command.code ? command.code : Constants.OCPP_ERROR_GENERIC_ERROR} on ${commandName}`, true);
7dde0b73
JB
668 // Build Exception
669 // eslint-disable-next-line no-empty-function
670 self._requests[messageId] = [() => { }, () => { }, '']; // Properly format the request
671 const error = reason instanceof OCPPError ? reason : new Error(reason);
672 // Send error
673 reject(error);
674 }
675 });
676 }
677
5ad8570f
JB
678 handleResponseBootNotification(payload) {
679 if (payload.status === 'Accepted') {
680 this._heartbeatInterval = payload.interval * 1000;
681 this._addConfigurationKey('HeartBeatInterval', Utils.convertToInt(payload.interval));
682 this._addConfigurationKey('HeartbeatInterval', Utils.convertToInt(payload.interval), false, false);
683 this._basicStartMessageSequence();
0a60c33c 684 } else if (payload.status === 'Pending') {
ead548f2 685 logger.info(this._logPrefix() + ' Charging station pending on the central server');
5ad8570f 686 } else {
ead548f2 687 logger.info(this._logPrefix() + ' Charging station rejected by the central server');
7dde0b73 688 }
7dde0b73
JB
689 }
690
bec64e8b
JB
691 _initTransactionOnConnector(connectorId) {
692 this._connectors[connectorId].transactionStarted = false;
693 this._connectors[connectorId].transactionId = null;
694 this._connectors[connectorId].idTag = null;
695 this._connectors[connectorId].lastEnergyActiveImportRegisterValue = -1;
0a60c33c
JB
696 }
697
bec64e8b
JB
698 _resetTransactionOnConnector(connectorId) {
699 this._initTransactionOnConnector(connectorId);
700 if (this._connectors[connectorId].transactionSetInterval) {
701 clearInterval(this._connectors[connectorId].transactionSetInterval);
027b409a
JB
702 }
703 }
704
7dde0b73 705 handleResponseStartTransaction(payload, requestPayload) {
d3a7883e 706 if (this._connectors[requestPayload.connectorId].transactionStarted) {
ead548f2 707 logger.debug(this._logPrefix() + ' Try to start a transaction on an already used connector ' + requestPayload.connectorId + ': %s', this._connectors[requestPayload.connectorId]);
2d0e26f5 708 return;
d3a7883e 709 }
84393381 710
7de604f9
JB
711 let transactionConnectorId;
712 for (const connector in this._connectors) {
713 if (Utils.convertToInt(connector) === Utils.convertToInt(requestPayload.connectorId)) {
714 transactionConnectorId = connector;
715 break;
7dde0b73 716 }
7de604f9
JB
717 }
718 if (!transactionConnectorId) {
ead548f2 719 logger.error(this._logPrefix() + ' Try to start a transaction on a non existing connector Id ' + requestPayload.connectorId);
7de604f9
JB
720 return;
721 }
722 if (payload.idTagInfo && payload.idTagInfo.status === 'Accepted') {
723 this._connectors[transactionConnectorId].transactionStarted = true;
724 this._connectors[transactionConnectorId].transactionId = payload.transactionId;
725 this._connectors[transactionConnectorId].idTag = requestPayload.idTag;
9b25a525 726 this._connectors[transactionConnectorId].lastEnergyActiveImportRegisterValue = 0;
7de604f9 727 this.sendStatusNotification(requestPayload.connectorId, 'Charging');
ead548f2 728 logger.info(this._logPrefix() + ' Transaction ' + payload.transactionId + ' STARTED on ' + this._stationInfo.name + '#' + requestPayload.connectorId + ' for idTag ' + requestPayload.idTag);
7de604f9 729 const configuredMeterValueSampleInterval = this._getConfigurationKey('MeterValueSampleInterval');
5ad8570f 730 this._startMeterValues(requestPayload.connectorId,
7de604f9 731 configuredMeterValueSampleInterval ? configuredMeterValueSampleInterval.value * 1000 : 60000);
7dde0b73 732 } else {
ead548f2 733 logger.error(this._logPrefix() + ' Starting transaction id ' + payload.transactionId + ' REJECTED with status ' + payload.idTagInfo.status + ', idTag ' + requestPayload.idTag);
7de604f9 734 this._resetTransactionOnConnector(transactionConnectorId);
7dde0b73
JB
735 this.sendStatusNotification(requestPayload.connectorId, 'Available');
736 }
737 }
738
34dcb3b5 739 handleResponseStopTransaction(payload, requestPayload) {
d3a7883e
JB
740 let transactionConnectorId;
741 for (const connector in this._connectors) {
742 if (this._connectors[connector].transactionId === requestPayload.transactionId) {
743 transactionConnectorId = connector;
744 break;
745 }
746 }
747 if (!transactionConnectorId) {
ead548f2 748 logger.error(this._logPrefix() + ' Try to stop a non existing transaction ' + requestPayload.transactionId);
7de604f9 749 return;
d3a7883e
JB
750 }
751 if (payload.idTagInfo && payload.idTagInfo.status === 'Accepted') {
752 this.sendStatusNotification(transactionConnectorId, 'Available');
ead548f2 753 logger.info(this._logPrefix() + ' Transaction ' + requestPayload.transactionId + ' STOPPED on ' + this._stationInfo.name + '#' + transactionConnectorId);
d3a7883e 754 this._resetTransactionOnConnector(transactionConnectorId);
34dcb3b5 755 } else {
ead548f2 756 logger.error(this._logPrefix() + ' Stopping transaction id ' + requestPayload.transactionId + ' REJECTED with status ' + payload.idTagInfo.status);
34dcb3b5
JB
757 }
758 }
759
facd8ebd 760 handleResponseStatusNotification(payload, requestPayload) {
ead548f2 761 logger.debug(this._logPrefix() + ' Status notification response received: %j to StatusNotification request: %j', payload, requestPayload);
7dde0b73
JB
762 }
763
facd8ebd 764 handleResponseMeterValues(payload, requestPayload) {
ead548f2 765 logger.debug(this._logPrefix() + ' MeterValues response received: %j to MeterValues request: %j', payload, requestPayload);
027b409a
JB
766 }
767
facd8ebd 768 handleResponseHeartbeat(payload, requestPayload) {
ead548f2 769 logger.debug(this._logPrefix() + ' Heartbeat response received: %j to Heartbeat request: %j', payload, requestPayload);
7dde0b73
JB
770 }
771
772 async handleRequest(messageId, commandName, commandPayload) {
773 let result;
774 this._statistics.addMessage(commandName, true);
775 // Call
776 if (typeof this['handle' + commandName] === 'function') {
777 try {
778 // Call the method
779 result = await this['handle' + commandName](commandPayload);
780 } catch (error) {
781 // Log
ead548f2 782 logger.error(this._logPrefix() + ' Handle request error: ' + error);
facd8ebd 783 // Send back response to inform backend
7dde0b73
JB
784 await this.sendError(messageId, error);
785 }
786 } else {
84393381 787 // Throw exception
f7869514 788 await this.sendError(messageId, new OCPPError(Constants.OCPP_ERROR_NOT_IMPLEMENTED, 'Not implemented', {}));
7dde0b73
JB
789 throw new Error(`${commandName} is not implemented ${JSON.stringify(commandPayload, null, ' ')}`);
790 }
84393381 791 // Send response
f7869514 792 await this.sendMessage(messageId, result, Constants.OCPP_JSON_CALL_RESULT_MESSAGE);
7dde0b73
JB
793 }
794
5ad8570f 795 async handleReset(commandPayload) {
5ad8570f
JB
796 // Simulate charging station restart
797 setImmediate(async () => {
2d0e26f5 798 await this.stop(commandPayload.type + 'Reset');
0a60c33c 799 await Utils.sleep(this._stationInfo.resetTime);
5ad8570f
JB
800 await this.start();
801 });
ead548f2 802 logger.info(`${this._logPrefix()} ${commandPayload.type} reset command received, simulating it. The station will be back online in ${this._stationInfo.resetTime}ms`);
5ad8570f
JB
803 return Constants.OCPP_RESPONSE_ACCEPTED;
804 }
805
61c2e33d
JB
806 _getConfigurationKey(key) {
807 return this._configuration.configurationKey.find((configElement) => configElement.key === key);
808 }
809
3497da01 810 _addConfigurationKey(key, value, readonly = false, visible = true, reboot = false) {
61c2e33d
JB
811 const keyFound = this._getConfigurationKey(key);
812 if (!keyFound) {
813 this._configuration.configurationKey.push({
814 key,
815 readonly,
816 value,
817 visible,
3497da01 818 reboot,
61c2e33d
JB
819 });
820 }
821 }
822
823 _setConfigurationKeyValue(key, value) {
824 const keyFound = this._getConfigurationKey(key);
825 if (keyFound) {
d3a7883e
JB
826 const keyIndex = this._configuration.configurationKey.indexOf(keyFound);
827 this._configuration.configurationKey[keyIndex].value = value;
61c2e33d
JB
828 }
829 }
830
34dcb3b5 831 async handleGetConfiguration(commandPayload) {
facd8ebd
JB
832 const configurationKey = [];
833 const unknownKey = [];
61c2e33d
JB
834 if (Utils.isEmptyArray(commandPayload.key)) {
835 for (const configuration of this._configuration.configurationKey) {
836 if (Utils.isUndefined(configuration.visible)) {
837 configuration.visible = true;
838 } else {
839 configuration.visible = Utils.convertToBoolean(configuration.visible);
840 }
841 if (!configuration.visible) {
842 continue;
843 }
844 configurationKey.push({
845 key: configuration.key,
846 readonly: configuration.readonly,
847 value: configuration.value,
848 });
facd8ebd 849 }
61c2e33d 850 } else {
d20c21a0
JB
851 for (const configurationKey of commandPayload.key) {
852 const keyFound = this._getConfigurationKey(configurationKey);
61c2e33d
JB
853 if (keyFound) {
854 if (Utils.isUndefined(keyFound.visible)) {
855 keyFound.visible = true;
856 } else {
d20c21a0 857 keyFound.visible = Utils.convertToBoolean(configurationKey.visible);
61c2e33d
JB
858 }
859 if (!keyFound.visible) {
860 continue;
861 }
862 configurationKey.push({
863 key: keyFound.key,
864 readonly: keyFound.readonly,
865 value: keyFound.value,
866 });
867 } else {
d20c21a0 868 unknownKey.push(configurationKey);
61c2e33d 869 }
facd8ebd 870 }
facd8ebd
JB
871 }
872 return {
873 configurationKey,
874 unknownKey,
875 };
7dde0b73
JB
876 }
877
878 async handleChangeConfiguration(commandPayload) {
61c2e33d 879 const keyToChange = this._getConfigurationKey(commandPayload.key);
7d887a1b
JB
880 if (!keyToChange) {
881 return {status: Constants.OCPP_ERROR_NOT_SUPPORTED};
882 } else if (keyToChange && Utils.convertToBoolean(keyToChange.readonly)) {
883 return Constants.OCPP_RESPONSE_REJECTED;
884 } else if (keyToChange && !Utils.convertToBoolean(keyToChange.readonly)) {
a6e68f34
JB
885 const keyIndex = this._configuration.configurationKey.indexOf(keyToChange);
886 this._configuration.configurationKey[keyIndex].value = commandPayload.value;
d3a7883e
JB
887 let triggerHeartbeatRestart = false;
888 if (keyToChange.key === 'HeartBeatInterval') {
889 this._setConfigurationKeyValue('HeartbeatInterval', commandPayload.value);
890 triggerHeartbeatRestart = true;
891 }
892 if (keyToChange.key === 'HeartbeatInterval') {
893 this._setConfigurationKeyValue('HeartBeatInterval', commandPayload.value);
894 triggerHeartbeatRestart = true;
895 }
896 if (triggerHeartbeatRestart) {
5c68da4d
JB
897 this._heartbeatInterval = Utils.convertToInt(commandPayload.value) * 1000;
898 // Stop heartbeat
0a60c33c 899 this._stopHeartbeat();
5c68da4d
JB
900 // Start heartbeat
901 this._startHeartbeat(this);
902 }
7d887a1b
JB
903 if (Utils.convertToBoolean(keyToChange.reboot)) {
904 return Constants.OCPP_RESPONSE_REBOOT_REQUIRED;
905 }
dcab13bd 906 return Constants.OCPP_RESPONSE_ACCEPTED;
7dde0b73 907 }
7dde0b73
JB
908 }
909
910 async handleRemoteStartTransaction(commandPayload) {
72766a82 911 const transactionConnectorID = commandPayload.connectorId ? commandPayload.connectorId : '1';
2e6f5966 912 if (this.hasAuthorizedTags() && this._getLocalAuthListEnabled() && this._getAuthorizeRemoteTxRequests()) {
dcab13bd 913 // Check if authorized
2e6f5966 914 if (this._authorizedTags.find((value) => value === commandPayload.idTag)) {
7dde0b73 915 // Authorization successful start transaction
61c2e33d 916 this.sendStartTransactionWithTimeout(transactionConnectorID, commandPayload.idTag, Constants.START_TRANSACTION_TIMEOUT);
ead548f2 917 logger.debug(this._logPrefix() + ' Transaction remotely STARTED on ' + this._stationInfo.name + '#' + transactionConnectorID + ' for idTag ' + commandPayload.idTag);
dcab13bd 918 return Constants.OCPP_RESPONSE_ACCEPTED;
7dde0b73 919 }
ead548f2 920 logger.error(this._logPrefix() + ' Remote starting transaction REJECTED with status ' + commandPayload.idTagInfo.status + ', idTag ' + commandPayload.idTag);
dcab13bd 921 return Constants.OCPP_RESPONSE_REJECTED;
7dde0b73 922 }
dcab13bd 923 // No local authorization check required => start transaction
61c2e33d 924 this.sendStartTransactionWithTimeout(transactionConnectorID, commandPayload.idTag, Constants.START_TRANSACTION_TIMEOUT);
ead548f2 925 logger.debug(this._logPrefix() + ' Transaction remotely STARTED on ' + this._stationInfo.name + '#' + transactionConnectorID + ' for idTag ' + commandPayload.idTag);
027b409a
JB
926 return Constants.OCPP_RESPONSE_ACCEPTED;
927 }
928
929 async handleRemoteStopTransaction(commandPayload) {
930 for (const connector in this._connectors) {
931 if (this._connectors[connector].transactionId === commandPayload.transactionId) {
d3a7883e
JB
932 this.sendStopTransaction(commandPayload.transactionId);
933 return Constants.OCPP_RESPONSE_ACCEPTED;
027b409a
JB
934 }
935 }
ead548f2 936 logger.info(this._logPrefix() + ' Try to stop remotely a non existing transaction ' + commandPayload.transactionId);
d3a7883e 937 return Constants.OCPP_RESPONSE_REJECTED;
7dde0b73 938 }
7dde0b73
JB
939}
940
941module.exports = ChargingStation;