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