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