76d75f574adade2877a89f035630cf040cbada66
[e-mobility-charging-stations-simulator.git] / src / charging-station / ChargingStation.js
1 import {PerformanceObserver, performance} from 'perf_hooks';
2
3 import AutomaticTransactionGenerator from './AutomaticTransactionGenerator.js';
4 import Configuration from '../utils/Configuration.js';
5 import Constants from '../utils/Constants.js';
6 import ElectricUtils from '../utils/ElectricUtils.js';
7 import OCPPError from './OcppError.js';
8 import Statistics from '../utils/Statistics.js';
9 import Utils from '../utils/Utils.js';
10 import WebSocket from 'ws';
11 import crypto from 'crypto';
12 import fs from 'fs';
13 import logger from '../utils/Logger.js';
14
15 export default class ChargingStation {
16 constructor(index, stationTemplateFile) {
17 this._index = index;
18 this._stationTemplateFile = stationTemplateFile;
19 this._connectors = {};
20 this._initialize();
21
22 this._isSocketRestart = false;
23 this._autoReconnectRetryCount = 0;
24 this._autoReconnectMaxRetries = Configuration.getAutoReconnectMaxRetries(); // -1 for unlimited
25 this._autoReconnectTimeout = Configuration.getAutoReconnectTimeout() * 1000; // ms, zero for disabling
26
27 this._requests = {};
28 this._messageQueue = [];
29
30 this._authorizedTags = this._loadAndGetAuthorizedTags();
31 }
32
33 _getStationName(stationTemplate) {
34 return stationTemplate.fixedName ? stationTemplate.baseName : stationTemplate.baseName + '-' + ('000000000' + this._index).substr(('000000000' + this._index).length - 4);
35 }
36
37 _buildStationInfo() {
38 let stationTemplateFromFile;
39 try {
40 // Load template file
41 const fileDescriptor = fs.openSync(this._stationTemplateFile, 'r');
42 stationTemplateFromFile = JSON.parse(fs.readFileSync(fileDescriptor, 'utf8'));
43 fs.closeSync(fileDescriptor);
44 } catch (error) {
45 logger.error('Template file ' + this._stationTemplateFile + ' loading error: ' + error);
46 throw error;
47 }
48 const stationTemplate = stationTemplateFromFile || {};
49 if (!Utils.isEmptyArray(stationTemplateFromFile.power)) {
50 stationTemplate.maxPower = stationTemplateFromFile.power[Math.floor(Math.random() * stationTemplateFromFile.power.length)];
51 } else {
52 stationTemplate.maxPower = stationTemplateFromFile.power;
53 }
54 stationTemplate.name = this._getStationName(stationTemplateFromFile);
55 stationTemplate.resetTime = stationTemplateFromFile.resetTime ? stationTemplateFromFile.resetTime * 1000 : Constants.CHARGING_STATION_DEFAULT_RESET_TIME;
56 return stationTemplate;
57 }
58
59 _initialize() {
60 this._stationInfo = this._buildStationInfo();
61 this._bootNotificationMessage = {
62 chargePointModel: this._stationInfo.chargePointModel,
63 chargePointVendor: this._stationInfo.chargePointVendor,
64 ...!Utils.isUndefined(this._stationInfo.chargeBoxSerialNumberPrefix) && {chargeBoxSerialNumber: this._stationInfo.chargeBoxSerialNumberPrefix},
65 ...!Utils.isUndefined(this._stationInfo.firmwareVersion) && {firmwareVersion: this._stationInfo.firmwareVersion},
66 };
67 this._configuration = this._getConfiguration();
68 this._supervisionUrl = this._getSupervisionURL();
69 this._wsConnectionUrl = this._supervisionUrl + '/' + this._stationInfo.name;
70 // Build connectors if needed
71 const maxConnectors = this._getMaxNumberOfConnectors();
72 if (maxConnectors <= 0) {
73 logger.warn(`${this._logPrefix()} Charging station template ${this._stationTemplateFile} with ${maxConnectors} connectors`);
74 }
75 const templateMaxConnectors = this._getTemplateMaxNumberOfConnectors();
76 if (templateMaxConnectors <= 0) {
77 logger.warn(`${this._logPrefix()} Charging station template ${this._stationTemplateFile} with no connector configurations`);
78 }
79 // Sanity check
80 if (maxConnectors > (this._stationInfo.Connectors[0] ? templateMaxConnectors - 1 : templateMaxConnectors) && !Utils.convertToBoolean(this._stationInfo.randomConnectors)) {
81 logger.warn(`${this._logPrefix()} Number of connectors exceeds the number of connector configurations in template ${this._stationTemplateFile}, forcing random connector configurations affectation`);
82 this._stationInfo.randomConnectors = true;
83 }
84 const connectorsConfigHash = crypto.createHash('sha256').update(JSON.stringify(this._stationInfo.Connectors) + maxConnectors.toString()).digest('hex');
85 // FIXME: Handle shrinking the number of connectors
86 if (!this._connectors || (this._connectors && this._connectorsConfigurationHash !== connectorsConfigHash)) {
87 this._connectorsConfigurationHash = connectorsConfigHash;
88 // Add connector Id 0
89 let lastConnector = 0;
90 for (lastConnector in this._stationInfo.Connectors) {
91 if (Utils.convertToInt(lastConnector) === 0 && Utils.convertToBoolean(this._stationInfo.useConnectorId0) && this._stationInfo.Connectors[lastConnector]) {
92 this._connectors[lastConnector] = Utils.cloneObject(this._stationInfo.Connectors[lastConnector]);
93 }
94 }
95 // Generate all connectors
96 if ((this._stationInfo.Connectors[0] ? templateMaxConnectors - 1 : templateMaxConnectors) > 0) {
97 for (let index = 1; index <= maxConnectors; index++) {
98 const randConnectorID = Utils.convertToBoolean(this._stationInfo.randomConnectors) ? Utils.getRandomInt(lastConnector, 1) : index;
99 this._connectors[index] = Utils.cloneObject(this._stationInfo.Connectors[randConnectorID]);
100 }
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 // OCPP parameters
112 this._addConfigurationKey('NumberOfConnectors', this._getNumberOfConnectors(), true);
113 if (!this._getConfigurationKey('MeterValuesSampledData')) {
114 this._addConfigurationKey('MeterValuesSampledData', 'Energy.Active.Import.Register');
115 }
116 this._stationInfo.powerDivider = this._getPowerDivider();
117 if (this.getEnableStatistics()) {
118 this._statistics = Statistics.getInstance();
119 this._statistics.objName = this._stationInfo.name;
120 this._performanceObserver = new PerformanceObserver((list) => {
121 const entry = list.getEntries()[0];
122 this._statistics.logPerformance(entry, 'ChargingStation');
123 this._performanceObserver.disconnect();
124 });
125 }
126 }
127
128 _logPrefix() {
129 return Utils.logPrefix(` ${this._stationInfo.name}:`);
130 }
131
132 _getConfiguration() {
133 return this._stationInfo.Configuration ? this._stationInfo.Configuration : {};
134 }
135
136 _getAuthorizationFile() {
137 return this._stationInfo.authorizationFile ? this._stationInfo.authorizationFile : '';
138 }
139
140 _loadAndGetAuthorizedTags() {
141 let authorizedTags = [];
142 const authorizationFile = this._getAuthorizationFile();
143 if (authorizationFile) {
144 try {
145 // Load authorization file
146 const fileDescriptor = fs.openSync(authorizationFile, 'r');
147 authorizedTags = JSON.parse(fs.readFileSync(fileDescriptor, 'utf8'));
148 fs.closeSync(fileDescriptor);
149 } catch (error) {
150 logger.error(this._logPrefix() + ' Authorization file ' + authorizationFile + ' loading error: ' + error);
151 throw error;
152 }
153 } else {
154 logger.info(this._logPrefix() + ' No authorization file given in template file ' + this._stationTemplateFile);
155 }
156 return authorizedTags;
157 }
158
159 getRandomTagId() {
160 const index = Math.floor(Math.random() * this._authorizedTags.length);
161 return this._authorizedTags[index];
162 }
163
164 hasAuthorizedTags() {
165 return !Utils.isEmptyArray(this._authorizedTags);
166 }
167
168 getEnableStatistics() {
169 return !Utils.isUndefined(this._stationInfo.enableStatistics) ? Utils.convertToBoolean(this._stationInfo.enableStatistics) : true;
170 }
171
172 _getNumberOfPhases() {
173 switch (this._getPowerOutType()) {
174 case 'AC':
175 return !Utils.isUndefined(this._stationInfo.numberOfPhases) ? Utils.convertToInt(this._stationInfo.numberOfPhases) : 3;
176 case 'DC':
177 return 0;
178 }
179 }
180
181 _getNumberOfRunningTransactions() {
182 let trxCount = 0;
183 for (const connector in this._connectors) {
184 if (this.getConnector(connector).transactionStarted) {
185 trxCount++;
186 }
187 }
188 return trxCount;
189 }
190
191 _getPowerDivider() {
192 let powerDivider = this._getNumberOfConnectors();
193 if (this._stationInfo.powerSharedByConnectors) {
194 powerDivider = this._getNumberOfRunningTransactions();
195 }
196 return powerDivider;
197 }
198
199 getConnector(id) {
200 return this._connectors[Utils.convertToInt(id)];
201 }
202
203 _getTemplateMaxNumberOfConnectors() {
204 return Object.keys(this._stationInfo.Connectors).length;
205 }
206
207 _getMaxNumberOfConnectors() {
208 let maxConnectors = 0;
209 if (!Utils.isEmptyArray(this._stationInfo.numberOfConnectors)) {
210 // Distribute evenly the number of connectors
211 maxConnectors = this._stationInfo.numberOfConnectors[(this._index - 1) % this._stationInfo.numberOfConnectors.length];
212 } else if (!Utils.isUndefined(this._stationInfo.numberOfConnectors)) {
213 maxConnectors = this._stationInfo.numberOfConnectors;
214 } else {
215 maxConnectors = this._stationInfo.Connectors[0] ? this._getTemplateMaxNumberOfConnectors() - 1 : this._getTemplateMaxNumberOfConnectors();
216 }
217 return maxConnectors;
218 }
219
220 _getNumberOfConnectors() {
221 return this._connectors[0] ? Object.keys(this._connectors).length - 1 : Object.keys(this._connectors).length;
222 }
223
224 _getVoltageOut() {
225 const errMsg = `${this._logPrefix()} Unknown ${this._getPowerOutType()} powerOutType in template file ${this._stationTemplateFile}, cannot define default voltage out`;
226 let defaultVoltageOut;
227 switch (this._getPowerOutType()) {
228 case 'AC':
229 defaultVoltageOut = 230;
230 break;
231 case 'DC':
232 defaultVoltageOut = 400;
233 break;
234 default:
235 logger.error(errMsg);
236 throw Error(errMsg);
237 }
238 return !Utils.isUndefined(this._stationInfo.voltageOut) ? Utils.convertToInt(this._stationInfo.voltageOut) : defaultVoltageOut;
239 }
240
241 _getPowerOutType() {
242 return !Utils.isUndefined(this._stationInfo.powerOutType) ? this._stationInfo.powerOutType : 'AC';
243 }
244
245 _getSupervisionURL() {
246 const supervisionUrls = Utils.cloneObject(this._stationInfo.supervisionURL ? this._stationInfo.supervisionURL : Configuration.getSupervisionURLs());
247 let indexUrl = 0;
248 if (!Utils.isEmptyArray(supervisionUrls)) {
249 if (Configuration.getDistributeStationToTenantEqually()) {
250 indexUrl = this._index % supervisionUrls.length;
251 } else {
252 // Get a random url
253 indexUrl = Math.floor(Math.random() * supervisionUrls.length);
254 }
255 return supervisionUrls[indexUrl];
256 }
257 return supervisionUrls;
258 }
259
260 _getAuthorizeRemoteTxRequests() {
261 const authorizeRemoteTxRequests = this._getConfigurationKey('AuthorizeRemoteTxRequests');
262 return authorizeRemoteTxRequests ? Utils.convertToBoolean(authorizeRemoteTxRequests.value) : false;
263 }
264
265 _getLocalAuthListEnabled() {
266 const localAuthListEnabled = this._getConfigurationKey('LocalAuthListEnabled');
267 return localAuthListEnabled ? Utils.convertToBoolean(localAuthListEnabled.value) : false;
268 }
269
270 async _basicStartMessageSequence() {
271 // Start heartbeat
272 this._startHeartbeat(this);
273 // Initialize connectors status
274 for (const connector in this._connectors) {
275 if (!this.getConnector(connector).transactionStarted) {
276 if (this.getConnector(connector).bootStatus) {
277 this.sendStatusNotificationWithTimeout(connector, this.getConnector(connector).bootStatus);
278 } else {
279 this.sendStatusNotificationWithTimeout(connector, 'Available');
280 }
281 } else {
282 this.sendStatusNotificationWithTimeout(connector, 'Charging');
283 }
284 }
285 // Start the ATG
286 if (Utils.convertToBoolean(this._stationInfo.AutomaticTransactionGenerator.enable)) {
287 if (!this._automaticTransactionGeneration) {
288 this._automaticTransactionGeneration = new AutomaticTransactionGenerator(this);
289 }
290 if (this._automaticTransactionGeneration.timeToStop) {
291 this._automaticTransactionGeneration.start();
292 }
293 }
294 if (this.getEnableStatistics()) {
295 this._statistics.start();
296 }
297 }
298
299 // eslint-disable-next-line class-methods-use-this
300 async _startHeartbeat(self) {
301 if (self._heartbeatInterval && self._heartbeatInterval > 0 && !self._heartbeatSetInterval) {
302 self._heartbeatSetInterval = setInterval(() => {
303 this.sendHeartbeat();
304 }, self._heartbeatInterval);
305 logger.info(self._logPrefix() + ' Heartbeat started every ' + self._heartbeatInterval + 'ms');
306 } else {
307 logger.error(`${self._logPrefix()} Heartbeat interval set to ${self._heartbeatInterval}ms, not starting the heartbeat`);
308 }
309 }
310
311 async _stopHeartbeat() {
312 if (this._heartbeatSetInterval) {
313 clearInterval(this._heartbeatSetInterval);
314 this._heartbeatSetInterval = null;
315 }
316 }
317
318 _startAuthorizationFileMonitoring() {
319 // eslint-disable-next-line no-unused-vars
320 fs.watchFile(this._getAuthorizationFile(), (current, previous) => {
321 try {
322 logger.debug(this._logPrefix() + ' Authorization file ' + this._getAuthorizationFile() + ' have changed, reload');
323 // Initialize _authorizedTags
324 this._authorizedTags = this._loadAndGetAuthorizedTags();
325 } catch (error) {
326 logger.error(this._logPrefix() + ' Authorization file monitoring error: ' + error);
327 }
328 });
329 }
330
331 _startStationTemplateFileMonitoring() {
332 // eslint-disable-next-line no-unused-vars
333 fs.watchFile(this._stationTemplateFile, (current, previous) => {
334 try {
335 logger.debug(this._logPrefix() + ' Template file ' + this._stationTemplateFile + ' have changed, reload');
336 // Initialize
337 this._initialize();
338 this._addConfigurationKey('HeartBeatInterval', Utils.convertToInt(this._heartbeatInterval ? this._heartbeatInterval / 1000 : 0));
339 this._addConfigurationKey('HeartbeatInterval', Utils.convertToInt(this._heartbeatInterval ? this._heartbeatInterval / 1000 : 0), false, false);
340 } catch (error) {
341 logger.error(this._logPrefix() + ' Charging station template file monitoring error: ' + error);
342 }
343 });
344 }
345
346 async _startMeterValues(connectorId, interval) {
347 if (!this.getConnector(connectorId).transactionStarted) {
348 logger.error(`${this._logPrefix()} Trying to start MeterValues on connector Id ${connectorId} with no transaction started`);
349 return;
350 } else if (this.getConnector(connectorId).transactionStarted && !this.getConnector(connectorId).transactionId) {
351 logger.error(`${this._logPrefix()} Trying to start MeterValues on connector Id ${connectorId} with no transaction id`);
352 return;
353 }
354 if (interval > 0) {
355 this.getConnector(connectorId).transactionSetInterval = setInterval(async () => {
356 if (this.getEnableStatistics()) {
357 const sendMeterValues = performance.timerify(this.sendMeterValues);
358 this._performanceObserver.observe({
359 entryTypes: ['function'],
360 });
361 await sendMeterValues(connectorId, interval, this);
362 } else {
363 await this.sendMeterValues(connectorId, interval, this);
364 }
365 }, interval);
366 } else {
367 logger.error(`${this._logPrefix()} Charging station MeterValueSampleInterval configuration set to ${interval}ms, not sending MeterValues`);
368 }
369 }
370
371 async start() {
372 if (!this._wsConnectionUrl) {
373 this._wsConnectionUrl = this._supervisionUrl + '/' + this._stationInfo.name;
374 }
375 this._wsConnection = new WebSocket(this._wsConnectionUrl, 'ocpp' + Constants.OCPP_VERSION_16);
376 logger.info(this._logPrefix() + ' Will communicate through URL ' + this._supervisionUrl);
377 // Monitor authorization file
378 this._startAuthorizationFileMonitoring();
379 // Monitor station template file
380 this._startStationTemplateFileMonitoring();
381 // Handle Socket incoming messages
382 this._wsConnection.on('message', this.onMessage.bind(this));
383 // Handle Socket error
384 this._wsConnection.on('error', this.onError.bind(this));
385 // Handle Socket close
386 this._wsConnection.on('close', this.onClose.bind(this));
387 // Handle Socket opening connection
388 this._wsConnection.on('open', this.onOpen.bind(this));
389 // Handle Socket ping
390 this._wsConnection.on('ping', this.onPing.bind(this));
391 }
392
393 async stop(reason = '') {
394 // Stop heartbeat
395 await this._stopHeartbeat();
396 // Stop the ATG
397 if (Utils.convertToBoolean(this._stationInfo.AutomaticTransactionGenerator.enable) &&
398 this._automaticTransactionGeneration &&
399 !this._automaticTransactionGeneration.timeToStop) {
400 await this._automaticTransactionGeneration.stop(reason);
401 } else {
402 for (const connector in this._connectors) {
403 if (this.getConnector(connector).transactionStarted) {
404 await this.sendStopTransaction(this.getConnector(connector).transactionId, reason);
405 }
406 }
407 }
408 // eslint-disable-next-line guard-for-in
409 for (const connector in this._connectors) {
410 await this.sendStatusNotification(connector, 'Unavailable');
411 }
412 if (this._wsConnection && this._wsConnection.readyState === WebSocket.OPEN) {
413 await this._wsConnection.close();
414 }
415 }
416
417 _reconnect(error) {
418 logger.error(this._logPrefix() + ' Socket: abnormally closed', error);
419 // Stop the ATG if needed
420 if (Utils.convertToBoolean(this._stationInfo.AutomaticTransactionGenerator.enable) &&
421 Utils.convertToBoolean(this._stationInfo.AutomaticTransactionGenerator.stopOnConnectionFailure) &&
422 this._automaticTransactionGeneration &&
423 !this._automaticTransactionGeneration.timeToStop) {
424 this._automaticTransactionGeneration.stop();
425 }
426 // Stop heartbeat
427 this._stopHeartbeat();
428 if (this._autoReconnectTimeout !== 0 &&
429 (this._autoReconnectRetryCount < this._autoReconnectMaxRetries || this._autoReconnectMaxRetries === -1)) {
430 logger.error(`${this._logPrefix()} Socket: connection retry with timeout ${this._autoReconnectTimeout}ms`);
431 this._autoReconnectRetryCount++;
432 setTimeout(() => {
433 logger.error(this._logPrefix() + ' Socket: reconnecting try #' + this._autoReconnectRetryCount);
434 this.start();
435 }, this._autoReconnectTimeout);
436 } else if (this._autoReconnectTimeout !== 0 || this._autoReconnectMaxRetries !== -1) {
437 logger.error(`${this._logPrefix()} Socket: max retries reached (${this._autoReconnectRetryCount}) or retry disabled (${this._autoReconnectTimeout})`);
438 }
439 }
440
441 onOpen() {
442 logger.info(`${this._logPrefix()} Is connected to server through ${this._wsConnectionUrl}`);
443 if (!this._isSocketRestart) {
444 // Send BootNotification
445 this.sendBootNotification();
446 }
447 if (this._isSocketRestart) {
448 this._basicStartMessageSequence();
449 if (!Utils.isEmptyArray(this._messageQueue)) {
450 this._messageQueue.forEach((message) => {
451 if (this._wsConnection && this._wsConnection.readyState === WebSocket.OPEN) {
452 this._wsConnection.send(message);
453 }
454 });
455 }
456 }
457 this._autoReconnectRetryCount = 0;
458 this._isSocketRestart = false;
459 }
460
461 onError(error) {
462 switch (error) {
463 case 'ECONNREFUSED':
464 this._isSocketRestart = true;
465 this._reconnect(error);
466 break;
467 default:
468 logger.error(this._logPrefix() + ' Socket error: ' + error);
469 break;
470 }
471 }
472
473 onClose(error) {
474 switch (error) {
475 case 1000: // Normal close
476 case 1005:
477 logger.info(this._logPrefix() + ' Socket normally closed ' + error);
478 this._autoReconnectRetryCount = 0;
479 break;
480 default: // Abnormal close
481 this._isSocketRestart = true;
482 this._reconnect(error);
483 break;
484 }
485 }
486
487 onPing() {
488 logger.debug(this._logPrefix() + ' Has received a WS ping (rfc6455) from the server');
489 }
490
491 async onMessage(message) {
492 let [messageType, messageId, commandName, commandPayload, errorDetails] = [0, '', Constants.ENTITY_CHARGING_STATION, '', ''];
493 try {
494 // Parse the message
495 [messageType, messageId, commandName, commandPayload, errorDetails] = JSON.parse(message);
496
497 // Check the Type of message
498 switch (messageType) {
499 // Incoming Message
500 case Constants.OCPP_JSON_CALL_MESSAGE:
501 // Process the call
502 await this.handleRequest(messageId, commandName, commandPayload);
503 break;
504 // Outcome Message
505 case Constants.OCPP_JSON_CALL_RESULT_MESSAGE:
506 // Respond
507 // eslint-disable-next-line no-case-declarations
508 let responseCallback; let requestPayload;
509 if (Utils.isIterable(this._requests[messageId])) {
510 [responseCallback, , requestPayload] = this._requests[messageId];
511 } else {
512 throw new Error(`Response request for message id ${messageId} is not iterable`);
513 }
514 if (!responseCallback) {
515 // Error
516 throw new Error(`Response for unknown message id ${messageId}`);
517 }
518 delete this._requests[messageId];
519 responseCallback(commandName, requestPayload);
520 break;
521 // Error Message
522 case Constants.OCPP_JSON_CALL_ERROR_MESSAGE:
523 if (!this._requests[messageId]) {
524 // Error
525 throw new Error(`Error for unknown message id ${messageId}`);
526 }
527 // eslint-disable-next-line no-case-declarations
528 let rejectCallback;
529 if (Utils.isIterable(this._requests[messageId])) {
530 [, rejectCallback] = this._requests[messageId];
531 } else {
532 throw new Error(`Error request for message id ${messageId} is not iterable`);
533 }
534 delete this._requests[messageId];
535 rejectCallback(new OCPPError(commandName, commandPayload, errorDetails));
536 break;
537 // Error
538 default:
539 throw new Error(`Wrong message type ${messageType}`);
540 }
541 } catch (error) {
542 // Log
543 logger.error('%s Incoming message %j processing error %s on request content %s', this._logPrefix(), message, error, this._requests[messageId]);
544 // Send error
545 // await this.sendError(messageId, error);
546 }
547 }
548
549 sendHeartbeat() {
550 try {
551 const payload = {
552 currentTime: new Date().toISOString(),
553 };
554 this.sendMessage(Utils.generateUUID(), payload, Constants.OCPP_JSON_CALL_MESSAGE, 'Heartbeat');
555 } catch (error) {
556 logger.error(this._logPrefix() + ' Send Heartbeat error: ' + error);
557 throw error;
558 }
559 }
560
561 sendBootNotification() {
562 try {
563 this.sendMessage(Utils.generateUUID(), this._bootNotificationMessage, Constants.OCPP_JSON_CALL_MESSAGE, 'BootNotification');
564 } catch (error) {
565 logger.error(this._logPrefix() + ' Send BootNotification error: ' + error);
566 throw error;
567 }
568 }
569
570 async sendStatusNotification(connectorId, status, errorCode = 'NoError') {
571 try {
572 const payload = {
573 connectorId,
574 errorCode,
575 status,
576 };
577 await this.sendMessage(Utils.generateUUID(), payload, Constants.OCPP_JSON_CALL_MESSAGE, 'StatusNotification');
578 } catch (error) {
579 logger.error(this._logPrefix() + ' Send StatusNotification error: ' + error);
580 throw error;
581 }
582 }
583
584 sendStatusNotificationWithTimeout(connectorId, status, errorCode = 'NoError', timeout = Constants.STATUS_NOTIFICATION_TIMEOUT) {
585 setTimeout(() => this.sendStatusNotification(connectorId, status, errorCode), timeout);
586 }
587
588 async sendStartTransaction(connectorId, idTag) {
589 try {
590 const payload = {
591 connectorId,
592 idTag,
593 meterStart: 0,
594 timestamp: new Date().toISOString(),
595 };
596 return await this.sendMessage(Utils.generateUUID(), payload, Constants.OCPP_JSON_CALL_MESSAGE, 'StartTransaction');
597 } catch (error) {
598 logger.error(this._logPrefix() + ' Send StartTransaction error: ' + error);
599 throw error;
600 }
601 }
602
603 sendStartTransactionWithTimeout(connectorId, idTag, timeout = Constants.START_TRANSACTION_TIMEOUT) {
604 setTimeout(() => this.sendStartTransaction(connectorId, idTag), timeout);
605 }
606
607 async sendStopTransaction(transactionId, reason = '') {
608 try {
609 const payload = {
610 transactionId,
611 meterStop: 0,
612 timestamp: new Date().toISOString(),
613 ...reason && {reason},
614 };
615 await this.sendMessage(Utils.generateUUID(), payload, Constants.OCPP_JSON_CALL_MESSAGE, 'StopTransaction');
616 } catch (error) {
617 logger.error(this._logPrefix() + ' Send StopTransaction error: ' + error);
618 throw error;
619 }
620 }
621
622 // eslint-disable-next-line class-methods-use-this
623 async sendMeterValues(connectorId, interval, self, debug = false) {
624 try {
625 const sampledValues = {
626 timestamp: new Date().toISOString(),
627 sampledValue: [],
628 };
629 const meterValuesTemplate = self.getConnector(connectorId).MeterValues;
630 for (let index = 0; index < meterValuesTemplate.length; index++) {
631 const connector = self.getConnector(connectorId);
632 // SoC measurand
633 if (meterValuesTemplate[index].measurand && meterValuesTemplate[index].measurand === 'SoC' && self._getConfigurationKey('MeterValuesSampledData').value.includes('SoC')) {
634 sampledValues.sampledValue.push({
635 ...!Utils.isUndefined(meterValuesTemplate[index].unit) ? {unit: meterValuesTemplate[index].unit} : {unit: 'Percent'},
636 ...!Utils.isUndefined(meterValuesTemplate[index].context) && {context: meterValuesTemplate[index].context},
637 measurand: meterValuesTemplate[index].measurand,
638 ...!Utils.isUndefined(meterValuesTemplate[index].location) ? {location: meterValuesTemplate[index].location} : {location: 'EV'},
639 ...!Utils.isUndefined(meterValuesTemplate[index].value) ? {value: meterValuesTemplate[index].value} : {value: Utils.getRandomInt(100)},
640 });
641 const sampledValuesIndex = sampledValues.sampledValue.length - 1;
642 if (sampledValues.sampledValue[sampledValuesIndex].value > 100 || debug) {
643 logger.error(`${self._logPrefix()} MeterValues measurand ${sampledValues.sampledValue[sampledValuesIndex].measurand ? sampledValues.sampledValue[sampledValuesIndex].measurand : 'Energy.Active.Import.Register'}: connectorId ${connectorId}, transaction ${connector.transactionId}, value: ${sampledValues.sampledValue[sampledValuesIndex].value}/100`);
644 }
645 // Voltage measurand
646 } else if (meterValuesTemplate[index].measurand && meterValuesTemplate[index].measurand === 'Voltage' && self._getConfigurationKey('MeterValuesSampledData').value.includes('Voltage')) {
647 sampledValues.sampledValue.push({
648 ...!Utils.isUndefined(meterValuesTemplate[index].unit) ? {unit: meterValuesTemplate[index].unit} : {unit: 'V'},
649 ...!Utils.isUndefined(meterValuesTemplate[index].context) && {context: meterValuesTemplate[index].context},
650 measurand: meterValuesTemplate[index].measurand,
651 ...!Utils.isUndefined(meterValuesTemplate[index].location) && {location: meterValuesTemplate[index].location},
652 ...!Utils.isUndefined(meterValuesTemplate[index].value) ? {value: meterValuesTemplate[index].value} : {value: self._getVoltageOut()},
653 });
654 for (let phase = 1; self._getNumberOfPhases() === 3 && phase <= self._getNumberOfPhases(); phase++) {
655 const voltageValue = sampledValues.sampledValue[sampledValues.sampledValue.length - 1].value;
656 let phaseValue;
657 if (voltageValue >= 0 && voltageValue <= 250) {
658 phaseValue = `L${phase}-N`;
659 } else if (voltageValue > 250) {
660 phaseValue = `L${phase}-L${(phase + 1) % self._getNumberOfPhases() !== 0 ? (phase + 1) % self._getNumberOfPhases() : self._getNumberOfPhases()}`;
661 }
662 sampledValues.sampledValue.push({
663 ...!Utils.isUndefined(meterValuesTemplate[index].unit) ? {unit: meterValuesTemplate[index].unit} : {unit: 'V'},
664 ...!Utils.isUndefined(meterValuesTemplate[index].context) && {context: meterValuesTemplate[index].context},
665 measurand: meterValuesTemplate[index].measurand,
666 ...!Utils.isUndefined(meterValuesTemplate[index].location) && {location: meterValuesTemplate[index].location},
667 ...!Utils.isUndefined(meterValuesTemplate[index].value) ? {value: meterValuesTemplate[index].value} : {value: self._getVoltageOut()},
668 phase: phaseValue,
669 });
670 }
671 // Power.Active.Import measurand
672 } else if (meterValuesTemplate[index].measurand && meterValuesTemplate[index].measurand === 'Power.Active.Import' && self._getConfigurationKey('MeterValuesSampledData').value.includes('Power.Active.Import')) {
673 // FIXME: factor out powerDivider checks
674 if (Utils.isUndefined(self._stationInfo.powerDivider)) {
675 const errMsg = `${self._logPrefix()} MeterValues measurand ${meterValuesTemplate[index].measurand ? meterValuesTemplate[index].measurand : 'Energy.Active.Import.Register'}: powerDivider is undefined`;
676 logger.error(errMsg);
677 throw Error(errMsg);
678 } else if (self._stationInfo.powerDivider && self._stationInfo.powerDivider <= 0) {
679 const errMsg = `${self._logPrefix()} MeterValues measurand ${meterValuesTemplate[index].measurand ? meterValuesTemplate[index].measurand : 'Energy.Active.Import.Register'}: powerDivider have zero or below value ${self._stationInfo.powerDivider}`;
680 logger.error(errMsg);
681 throw Error(errMsg);
682 }
683 const errMsg = `${self._logPrefix()} MeterValues measurand ${meterValuesTemplate[index].measurand ? meterValuesTemplate[index].measurand : 'Energy.Active.Import.Register'}: Unknown ${self._getPowerOutType()} powerOutType in template file ${self._stationTemplateFile}, cannot calculate ${meterValuesTemplate[index].measurand ? meterValuesTemplate[index].measurand : 'Energy.Active.Import.Register'} measurand value`;
684 const powerMeasurandValues = {};
685 const maxPower = Math.round(self._stationInfo.maxPower / self._stationInfo.powerDivider);
686 const maxPowerPerPhase = Math.round((self._stationInfo.maxPower / self._stationInfo.powerDivider) / self._getNumberOfPhases());
687 switch (self._getPowerOutType()) {
688 case 'AC':
689 if (Utils.isUndefined(meterValuesTemplate[index].value)) {
690 powerMeasurandValues.L1 = Utils.getRandomFloatRounded(maxPowerPerPhase);
691 powerMeasurandValues.L2 = 0;
692 powerMeasurandValues.L3 = 0;
693 if (self._getNumberOfPhases() === 3) {
694 powerMeasurandValues.L2 = Utils.getRandomFloatRounded(maxPowerPerPhase);
695 powerMeasurandValues.L3 = Utils.getRandomFloatRounded(maxPowerPerPhase);
696 }
697 powerMeasurandValues.all = Utils.roundTo(powerMeasurandValues.L1 + powerMeasurandValues.L2 + powerMeasurandValues.L3, 2);
698 }
699 break;
700 case 'DC':
701 if (Utils.isUndefined(meterValuesTemplate[index].value)) {
702 powerMeasurandValues.all = Utils.getRandomFloatRounded(maxPower);
703 }
704 break;
705 default:
706 logger.error(errMsg);
707 throw Error(errMsg);
708 }
709 sampledValues.sampledValue.push({
710 ...!Utils.isUndefined(meterValuesTemplate[index].unit) ? {unit: meterValuesTemplate[index].unit} : {unit: 'W'},
711 ...!Utils.isUndefined(meterValuesTemplate[index].context) && {context: meterValuesTemplate[index].context},
712 measurand: meterValuesTemplate[index].measurand,
713 ...!Utils.isUndefined(meterValuesTemplate[index].location) && {location: meterValuesTemplate[index].location},
714 ...!Utils.isUndefined(meterValuesTemplate[index].value) ? {value: meterValuesTemplate[index].value} : {value: powerMeasurandValues.all},
715 });
716 const sampledValuesIndex = sampledValues.sampledValue.length - 1;
717 if (sampledValues.sampledValue[sampledValuesIndex].value > maxPower || debug) {
718 logger.error(`${self._logPrefix()} MeterValues measurand ${sampledValues.sampledValue[sampledValuesIndex].measurand ? sampledValues.sampledValue[sampledValuesIndex].measurand : 'Energy.Active.Import.Register'}: connectorId ${connectorId}, transaction ${connector.transactionId}, value: ${sampledValues.sampledValue[sampledValuesIndex].value}/${maxPower}`);
719 }
720 for (let phase = 1; self._getNumberOfPhases() === 3 && phase <= self._getNumberOfPhases(); phase++) {
721 const phaseValue = `L${phase}-N`;
722 sampledValues.sampledValue.push({
723 ...!Utils.isUndefined(meterValuesTemplate[index].unit) ? {unit: meterValuesTemplate[index].unit} : {unit: 'W'},
724 ...!Utils.isUndefined(meterValuesTemplate[index].context) && {context: meterValuesTemplate[index].context},
725 ...!Utils.isUndefined(meterValuesTemplate[index].measurand) && {measurand: meterValuesTemplate[index].measurand},
726 ...!Utils.isUndefined(meterValuesTemplate[index].location) && {location: meterValuesTemplate[index].location},
727 ...!Utils.isUndefined(meterValuesTemplate[index].value) ? {value: meterValuesTemplate[index].value} : {value: powerMeasurandValues[`L${phase}`]},
728 phase: phaseValue,
729 });
730 }
731 // Current.Import measurand
732 } else if (meterValuesTemplate[index].measurand && meterValuesTemplate[index].measurand === 'Current.Import' && self._getConfigurationKey('MeterValuesSampledData').value.includes('Current.Import')) {
733 // FIXME: factor out powerDivider checks
734 if (Utils.isUndefined(self._stationInfo.powerDivider)) {
735 const errMsg = `${self._logPrefix()} MeterValues measurand ${meterValuesTemplate[index].measurand ? meterValuesTemplate[index].measurand : 'Energy.Active.Import.Register'}: powerDivider is undefined`;
736 logger.error(errMsg);
737 throw Error(errMsg);
738 } else if (self._stationInfo.powerDivider && self._stationInfo.powerDivider <= 0) {
739 const errMsg = `${self._logPrefix()} MeterValues measurand ${meterValuesTemplate[index].measurand ? meterValuesTemplate[index].measurand : 'Energy.Active.Import.Register'}: powerDivider have zero or below value ${self._stationInfo.powerDivider}`;
740 logger.error(errMsg);
741 throw Error(errMsg);
742 }
743 const errMsg = `${self._logPrefix()} MeterValues measurand ${meterValuesTemplate[index].measurand ? meterValuesTemplate[index].measurand : 'Energy.Active.Import.Register'}: Unknown ${self._getPowerOutType()} powerOutType in template file ${self._stationTemplateFile}, cannot calculate ${meterValuesTemplate[index].measurand ? meterValuesTemplate[index].measurand : 'Energy.Active.Import.Register'} measurand value`;
744 const currentMeasurandValues = {};
745 let maxAmperage;
746 switch (self._getPowerOutType()) {
747 case 'AC':
748 maxAmperage = ElectricUtils.ampPerPhaseFromPower(self._getNumberOfPhases(), self._stationInfo.maxPower / self._stationInfo.powerDivider, self._getVoltageOut());
749 if (Utils.isUndefined(meterValuesTemplate[index].value)) {
750 currentMeasurandValues.L1 = Utils.getRandomFloatRounded(maxAmperage);
751 currentMeasurandValues.L2 = 0;
752 currentMeasurandValues.L3 = 0;
753 if (self._getNumberOfPhases() === 3) {
754 currentMeasurandValues.L2 = Utils.getRandomFloatRounded(maxAmperage);
755 currentMeasurandValues.L3 = Utils.getRandomFloatRounded(maxAmperage);
756 }
757 currentMeasurandValues.all = Utils.roundTo((currentMeasurandValues.L1 + currentMeasurandValues.L2 + currentMeasurandValues.L3) / self._getNumberOfPhases(), 2);
758 }
759 break;
760 case 'DC':
761 maxAmperage = ElectricUtils.ampTotalFromPower(self._stationInfo.maxPower / self._stationInfo.powerDivider, self._getVoltageOut());
762 if (Utils.isUndefined(meterValuesTemplate[index].value)) {
763 currentMeasurandValues.all = Utils.getRandomFloatRounded(maxAmperage);
764 }
765 break;
766 default:
767 logger.error(errMsg);
768 throw Error(errMsg);
769 }
770 sampledValues.sampledValue.push({
771 ...!Utils.isUndefined(meterValuesTemplate[index].unit) ? {unit: meterValuesTemplate[index].unit} : {unit: 'A'},
772 ...!Utils.isUndefined(meterValuesTemplate[index].context) && {context: meterValuesTemplate[index].context},
773 measurand: meterValuesTemplate[index].measurand,
774 ...!Utils.isUndefined(meterValuesTemplate[index].location) && {location: meterValuesTemplate[index].location},
775 ...!Utils.isUndefined(meterValuesTemplate[index].value) ? {value: meterValuesTemplate[index].value} : {value: currentMeasurandValues.all},
776 });
777 const sampledValuesIndex = sampledValues.sampledValue.length - 1;
778 if (sampledValues.sampledValue[sampledValuesIndex].value > maxAmperage || debug) {
779 logger.error(`${self._logPrefix()} MeterValues measurand ${sampledValues.sampledValue[sampledValuesIndex].measurand ? sampledValues.sampledValue[sampledValuesIndex].measurand : 'Energy.Active.Import.Register'}: connectorId ${connectorId}, transaction ${connector.transactionId}, value: ${sampledValues.sampledValue[sampledValuesIndex].value}/${maxAmperage}`);
780 }
781 for (let phase = 1; self._getNumberOfPhases() === 3 && phase <= self._getNumberOfPhases(); phase++) {
782 const phaseValue = `L${phase}`;
783 sampledValues.sampledValue.push({
784 ...!Utils.isUndefined(meterValuesTemplate[index].unit) ? {unit: meterValuesTemplate[index].unit} : {unit: 'A'},
785 ...!Utils.isUndefined(meterValuesTemplate[index].context) && {context: meterValuesTemplate[index].context},
786 ...!Utils.isUndefined(meterValuesTemplate[index].measurand) && {measurand: meterValuesTemplate[index].measurand},
787 ...!Utils.isUndefined(meterValuesTemplate[index].location) && {location: meterValuesTemplate[index].location},
788 ...!Utils.isUndefined(meterValuesTemplate[index].value) ? {value: meterValuesTemplate[index].value} : {value: currentMeasurandValues[phaseValue]},
789 phase: phaseValue,
790 });
791 }
792 // Energy.Active.Import.Register measurand (default)
793 } else if (!meterValuesTemplate[index].measurand || meterValuesTemplate[index].measurand === 'Energy.Active.Import.Register') {
794 // FIXME: factor out powerDivider checks
795 if (Utils.isUndefined(self._stationInfo.powerDivider)) {
796 const errMsg = `${self._logPrefix()} MeterValues measurand ${meterValuesTemplate[index].measurand ? meterValuesTemplate[index].measurand : 'Energy.Active.Import.Register'}: powerDivider is undefined`;
797 logger.error(errMsg);
798 throw Error(errMsg);
799 } else if (self._stationInfo.powerDivider && self._stationInfo.powerDivider <= 0) {
800 const errMsg = `${self._logPrefix()} MeterValues measurand ${meterValuesTemplate[index].measurand ? meterValuesTemplate[index].measurand : 'Energy.Active.Import.Register'}: powerDivider have zero or below value ${self._stationInfo.powerDivider}`;
801 logger.error(errMsg);
802 throw Error(errMsg);
803 }
804 if (Utils.isUndefined(meterValuesTemplate[index].value)) {
805 const measurandValue = Utils.getRandomInt(self._stationInfo.maxPower / (self._stationInfo.powerDivider * 3600000) * interval);
806 // Persist previous value in connector
807 if (connector && !Utils.isNullOrUndefined(connector.lastEnergyActiveImportRegisterValue) && connector.lastEnergyActiveImportRegisterValue >= 0) {
808 connector.lastEnergyActiveImportRegisterValue += measurandValue;
809 } else {
810 connector.lastEnergyActiveImportRegisterValue = 0;
811 }
812 }
813 sampledValues.sampledValue.push({
814 ...!Utils.isUndefined(meterValuesTemplate[index].unit) ? {unit: meterValuesTemplate[index].unit} : {unit: 'Wh'},
815 ...!Utils.isUndefined(meterValuesTemplate[index].context) && {context: meterValuesTemplate[index].context},
816 ...!Utils.isUndefined(meterValuesTemplate[index].measurand) && {measurand: meterValuesTemplate[index].measurand},
817 ...!Utils.isUndefined(meterValuesTemplate[index].location) && {location: meterValuesTemplate[index].location},
818 ...!Utils.isUndefined(meterValuesTemplate[index].value) ? {value: meterValuesTemplate[index].value} : {value: connector.lastEnergyActiveImportRegisterValue},
819 });
820 const sampledValuesIndex = sampledValues.sampledValue.length - 1;
821 const maxConsumption = Math.round(self._stationInfo.maxPower * 3600 / (self._stationInfo.powerDivider * interval));
822 if (sampledValues.sampledValue[sampledValuesIndex].value > maxConsumption || debug) {
823 logger.error(`${self._logPrefix()} MeterValues measurand ${sampledValues.sampledValue[sampledValuesIndex].measurand ? sampledValues.sampledValue[sampledValuesIndex].measurand : 'Energy.Active.Import.Register'}: connectorId ${connectorId}, transaction ${connector.transactionId}, value: ${sampledValues.sampledValue[sampledValuesIndex].value}/${maxConsumption}`);
824 }
825 // Unsupported measurand
826 } else {
827 logger.info(`${self._logPrefix()} Unsupported MeterValues measurand ${meterValuesTemplate[index].measurand ? meterValuesTemplate[index].measurand : 'Energy.Active.Import.Register'} on connectorId ${connectorId}`);
828 }
829 }
830
831 const payload = {
832 connectorId,
833 transactionId: self.getConnector(connectorId).transactionId,
834 meterValue: sampledValues,
835 };
836 await self.sendMessage(Utils.generateUUID(), payload, Constants.OCPP_JSON_CALL_MESSAGE, 'MeterValues');
837 } catch (error) {
838 logger.error(self._logPrefix() + ' Send MeterValues error: ' + error);
839 throw error;
840 }
841 }
842
843 sendError(messageId, err) {
844 // Check exception: only OCPP error are accepted
845 const error = err instanceof OCPPError ? err : new OCPPError(Constants.OCPP_ERROR_INTERNAL_ERROR, err.message);
846 // Send error
847 return this.sendMessage(messageId, error, Constants.OCPP_JSON_CALL_ERROR_MESSAGE);
848 }
849
850 sendMessage(messageId, command, messageType = Constants.OCPP_JSON_CALL_RESULT_MESSAGE, commandName = '') {
851 // Send a message through wsConnection
852 const self = this;
853 // Create a promise
854 return new Promise((resolve, reject) => {
855 let messageToSend;
856 // Type of message
857 switch (messageType) {
858 // Request
859 case Constants.OCPP_JSON_CALL_MESSAGE:
860 if (this.getEnableStatistics()) {
861 this._statistics.addMessage(commandName);
862 }
863 // Build request
864 this._requests[messageId] = [responseCallback, rejectCallback, command];
865 messageToSend = JSON.stringify([messageType, messageId, commandName, command]);
866 break;
867 // Response
868 case Constants.OCPP_JSON_CALL_RESULT_MESSAGE:
869 if (this.getEnableStatistics()) {
870 this._statistics.addMessage(commandName);
871 }
872 // Build response
873 messageToSend = JSON.stringify([messageType, messageId, command]);
874 break;
875 // Error Message
876 case Constants.OCPP_JSON_CALL_ERROR_MESSAGE:
877 if (this.getEnableStatistics()) {
878 this._statistics.addMessage(`Error ${command.code ? command.code : Constants.OCPP_ERROR_GENERIC_ERROR} on ${commandName}`);
879 }
880 // Build Message
881 messageToSend = JSON.stringify([messageType, messageId, command.code ? command.code : Constants.OCPP_ERROR_GENERIC_ERROR, command.message ? command.message : '', command.details ? command.details : {}]);
882 break;
883 }
884 // Check if wsConnection is ready
885 if (this._wsConnection && this._wsConnection.readyState === WebSocket.OPEN) {
886 // Yes: Send Message
887 this._wsConnection.send(messageToSend);
888 } else {
889 // Buffer message until connection is back
890 this._messageQueue.push(messageToSend);
891 }
892 // Request?
893 if (messageType !== Constants.OCPP_JSON_CALL_MESSAGE) {
894 // Yes: send Ok
895 resolve();
896 } else if (this._wsConnection && this._wsConnection.readyState === WebSocket.OPEN) {
897 // Send timeout in case connection is open otherwise wait for ever
898 // FIXME: Handle message on timeout
899 setTimeout(() => rejectCallback(new OCPPError(command.code ? command.code : Constants.OCPP_ERROR_GENERIC_ERROR, command.message ? command.message : '', command.details ? command.details : {})), Constants.OCPP_SOCKET_TIMEOUT);
900 }
901
902 // Function that will receive the request's response
903 function responseCallback(payload, requestPayload) {
904 self.handleResponse(commandName, payload, requestPayload, self);
905 // Send the response
906 resolve(payload);
907 }
908
909 // Function that will receive the request's rejection
910 function rejectCallback(error) {
911 if (!(error instanceof OCPPError)) {
912 const errMsg = `${self._logPrefix()} Argument error is not an instance of OCPPError in rejectCallback call`;
913 logger.error(errMsg);
914 throw Error(errMsg);
915 }
916 logger.debug(`${self._logPrefix()} Error %j on commandName %s command %j`, error, commandName, command);
917 if (self.getEnableStatistics()) {
918 self._statistics.addMessage(`Error on commandName ${commandName}`, true);
919 }
920 // Build Exception
921 // eslint-disable-next-line no-empty-function
922 self._requests[messageId] = [() => { }, () => { }, '']; // Properly format the request
923 // Send error
924 reject(error);
925 }
926 });
927 }
928
929 // eslint-disable-next-line class-methods-use-this
930 handleResponse(commandName, payload, requestPayload, self) {
931 if (self.getEnableStatistics()) {
932 self._statistics.addMessage(commandName, true);
933 }
934 const responseCallbackFn = 'handleResponse' + commandName;
935 if (typeof self[responseCallbackFn] === 'function') {
936 self[responseCallbackFn](payload, requestPayload, self);
937 } else {
938 logger.error(self._logPrefix() + ' Trying to call an undefined response callback function: ' + responseCallbackFn);
939 }
940 }
941
942 handleResponseBootNotification(payload) {
943 if (payload.status === 'Accepted') {
944 this._heartbeatInterval = payload.interval * 1000;
945 this._addConfigurationKey('HeartBeatInterval', Utils.convertToInt(payload.interval));
946 this._addConfigurationKey('HeartbeatInterval', Utils.convertToInt(payload.interval), false, false);
947 this._basicStartMessageSequence();
948 } else if (payload.status === 'Pending') {
949 logger.info(this._logPrefix() + ' Charging station in pending state on the central server');
950 } else {
951 logger.info(this._logPrefix() + ' Charging station rejected by the central server');
952 }
953 }
954
955 _initTransactionOnConnector(connectorId) {
956 this.getConnector(connectorId).transactionStarted = false;
957 this.getConnector(connectorId).transactionId = null;
958 this.getConnector(connectorId).idTag = null;
959 this.getConnector(connectorId).lastEnergyActiveImportRegisterValue = -1;
960 }
961
962 _resetTransactionOnConnector(connectorId) {
963 this._initTransactionOnConnector(connectorId);
964 if (this.getConnector(connectorId).transactionSetInterval) {
965 clearInterval(this.getConnector(connectorId).transactionSetInterval);
966 }
967 }
968
969 handleResponseStartTransaction(payload, requestPayload) {
970 if (this.getConnector(requestPayload.connectorId).transactionStarted) {
971 logger.debug(this._logPrefix() + ' Try to start a transaction on an already used connector ' + requestPayload.connectorId + ': %s', this.getConnector(requestPayload.connectorId));
972 return;
973 }
974
975 let transactionConnectorId;
976 for (const connector in this._connectors) {
977 if (Utils.convertToInt(connector) === Utils.convertToInt(requestPayload.connectorId)) {
978 transactionConnectorId = connector;
979 break;
980 }
981 }
982 if (!transactionConnectorId) {
983 logger.error(this._logPrefix() + ' Try to start a transaction on a non existing connector Id ' + requestPayload.connectorId);
984 return;
985 }
986 if (payload.idTagInfo && payload.idTagInfo.status === 'Accepted') {
987 this.getConnector(requestPayload.connectorId).transactionStarted = true;
988 this.getConnector(requestPayload.connectorId).transactionId = payload.transactionId;
989 this.getConnector(requestPayload.connectorId).idTag = requestPayload.idTag;
990 this.getConnector(requestPayload.connectorId).lastEnergyActiveImportRegisterValue = 0;
991 this.sendStatusNotification(requestPayload.connectorId, 'Charging');
992 logger.info(this._logPrefix() + ' Transaction ' + payload.transactionId + ' STARTED on ' + this._stationInfo.name + '#' + requestPayload.connectorId + ' for idTag ' + requestPayload.idTag);
993 if (this._stationInfo.powerSharedByConnectors) {
994 this._stationInfo.powerDivider++;
995 }
996 const configuredMeterValueSampleInterval = this._getConfigurationKey('MeterValueSampleInterval');
997 this._startMeterValues(requestPayload.connectorId,
998 configuredMeterValueSampleInterval ? configuredMeterValueSampleInterval.value * 1000 : 60000);
999 } else {
1000 logger.error(this._logPrefix() + ' Starting transaction id ' + payload.transactionId + ' REJECTED with status ' + payload.idTagInfo.status + ', idTag ' + requestPayload.idTag);
1001 this._resetTransactionOnConnector(requestPayload.connectorId);
1002 this.sendStatusNotification(requestPayload.connectorId, 'Available');
1003 }
1004 }
1005
1006 handleResponseStopTransaction(payload, requestPayload) {
1007 let transactionConnectorId;
1008 for (const connector in this._connectors) {
1009 if (this.getConnector(connector).transactionId === requestPayload.transactionId) {
1010 transactionConnectorId = connector;
1011 break;
1012 }
1013 }
1014 if (!transactionConnectorId) {
1015 logger.error(this._logPrefix() + ' Try to stop a non existing transaction ' + requestPayload.transactionId);
1016 return;
1017 }
1018 if (payload.idTagInfo && payload.idTagInfo.status === 'Accepted') {
1019 this.sendStatusNotification(transactionConnectorId, 'Available');
1020 if (this._stationInfo.powerSharedByConnectors) {
1021 this._stationInfo.powerDivider--;
1022 }
1023 logger.info(this._logPrefix() + ' Transaction ' + requestPayload.transactionId + ' STOPPED on ' + this._stationInfo.name + '#' + transactionConnectorId);
1024 this._resetTransactionOnConnector(transactionConnectorId);
1025 } else {
1026 logger.error(this._logPrefix() + ' Stopping transaction id ' + requestPayload.transactionId + ' REJECTED with status ' + payload.idTagInfo.status);
1027 }
1028 }
1029
1030 handleResponseStatusNotification(payload, requestPayload) {
1031 logger.debug(this._logPrefix() + ' Status notification response received: %j to StatusNotification request: %j', payload, requestPayload);
1032 }
1033
1034 handleResponseMeterValues(payload, requestPayload) {
1035 logger.debug(this._logPrefix() + ' MeterValues response received: %j to MeterValues request: %j', payload, requestPayload);
1036 }
1037
1038 handleResponseHeartbeat(payload, requestPayload) {
1039 logger.debug(this._logPrefix() + ' Heartbeat response received: %j to Heartbeat request: %j', payload, requestPayload);
1040 }
1041
1042 async handleRequest(messageId, commandName, commandPayload) {
1043 if (this.getEnableStatistics()) {
1044 this._statistics.addMessage(commandName, true);
1045 }
1046 let response;
1047 // Call
1048 if (typeof this['handleRequest' + commandName] === 'function') {
1049 try {
1050 // Call the method to build the response
1051 response = await this['handleRequest' + commandName](commandPayload);
1052 } catch (error) {
1053 // Log
1054 logger.error(this._logPrefix() + ' Handle request error: ' + error);
1055 // Send back response to inform backend
1056 await this.sendError(messageId, error);
1057 }
1058 } else {
1059 // Throw exception
1060 await this.sendError(messageId, new OCPPError(Constants.OCPP_ERROR_NOT_IMPLEMENTED, 'Not implemented', {}));
1061 throw new Error(`${commandName} is not implemented ${JSON.stringify(commandPayload, null, ' ')}`);
1062 }
1063 // Send response
1064 await this.sendMessage(messageId, response, Constants.OCPP_JSON_CALL_RESULT_MESSAGE);
1065 }
1066
1067 // Simulate charging station restart
1068 async handleRequestReset(commandPayload) {
1069 setImmediate(async () => {
1070 await this.stop(commandPayload.type + 'Reset');
1071 await Utils.sleep(this._stationInfo.resetTime);
1072 await this.start();
1073 });
1074 logger.info(`${this._logPrefix()} ${commandPayload.type} reset command received, simulating it. The station will be back online in ${this._stationInfo.resetTime}ms`);
1075 return Constants.OCPP_RESPONSE_ACCEPTED;
1076 }
1077
1078 _getConfigurationKey(key) {
1079 return this._configuration.configurationKey.find((configElement) => configElement.key === key);
1080 }
1081
1082 _addConfigurationKey(key, value, readonly = false, visible = true, reboot = false) {
1083 const keyFound = this._getConfigurationKey(key);
1084 if (!keyFound) {
1085 this._configuration.configurationKey.push({
1086 key,
1087 readonly,
1088 value,
1089 visible,
1090 reboot,
1091 });
1092 }
1093 }
1094
1095 _setConfigurationKeyValue(key, value) {
1096 const keyFound = this._getConfigurationKey(key);
1097 if (keyFound) {
1098 const keyIndex = this._configuration.configurationKey.indexOf(keyFound);
1099 this._configuration.configurationKey[keyIndex].value = value;
1100 }
1101 }
1102
1103 async handleRequestGetConfiguration(commandPayload) {
1104 const configurationKey = [];
1105 const unknownKey = [];
1106 if (Utils.isEmptyArray(commandPayload.key)) {
1107 for (const configuration of this._configuration.configurationKey) {
1108 if (Utils.isUndefined(configuration.visible)) {
1109 configuration.visible = true;
1110 } else {
1111 configuration.visible = Utils.convertToBoolean(configuration.visible);
1112 }
1113 if (!configuration.visible) {
1114 continue;
1115 }
1116 configurationKey.push({
1117 key: configuration.key,
1118 readonly: configuration.readonly,
1119 value: configuration.value,
1120 });
1121 }
1122 } else {
1123 for (const configurationKey of commandPayload.key) {
1124 const keyFound = this._getConfigurationKey(configurationKey);
1125 if (keyFound) {
1126 if (Utils.isUndefined(keyFound.visible)) {
1127 keyFound.visible = true;
1128 } else {
1129 keyFound.visible = Utils.convertToBoolean(configurationKey.visible);
1130 }
1131 if (!keyFound.visible) {
1132 continue;
1133 }
1134 configurationKey.push({
1135 key: keyFound.key,
1136 readonly: keyFound.readonly,
1137 value: keyFound.value,
1138 });
1139 } else {
1140 unknownKey.push(configurationKey);
1141 }
1142 }
1143 }
1144 return {
1145 configurationKey,
1146 unknownKey,
1147 };
1148 }
1149
1150 async handleRequestChangeConfiguration(commandPayload) {
1151 const keyToChange = this._getConfigurationKey(commandPayload.key);
1152 if (!keyToChange) {
1153 return {status: Constants.OCPP_ERROR_NOT_SUPPORTED};
1154 } else if (keyToChange && Utils.convertToBoolean(keyToChange.readonly)) {
1155 return Constants.OCPP_RESPONSE_REJECTED;
1156 } else if (keyToChange && !Utils.convertToBoolean(keyToChange.readonly)) {
1157 const keyIndex = this._configuration.configurationKey.indexOf(keyToChange);
1158 this._configuration.configurationKey[keyIndex].value = commandPayload.value;
1159 let triggerHeartbeatRestart = false;
1160 if (keyToChange.key === 'HeartBeatInterval') {
1161 this._setConfigurationKeyValue('HeartbeatInterval', commandPayload.value);
1162 triggerHeartbeatRestart = true;
1163 }
1164 if (keyToChange.key === 'HeartbeatInterval') {
1165 this._setConfigurationKeyValue('HeartBeatInterval', commandPayload.value);
1166 triggerHeartbeatRestart = true;
1167 }
1168 if (triggerHeartbeatRestart) {
1169 this._heartbeatInterval = Utils.convertToInt(commandPayload.value) * 1000;
1170 // Stop heartbeat
1171 this._stopHeartbeat();
1172 // Start heartbeat
1173 this._startHeartbeat(this);
1174 }
1175 if (Utils.convertToBoolean(keyToChange.reboot)) {
1176 return Constants.OCPP_RESPONSE_REBOOT_REQUIRED;
1177 }
1178 return Constants.OCPP_RESPONSE_ACCEPTED;
1179 }
1180 }
1181
1182 async handleRequestRemoteStartTransaction(commandPayload) {
1183 const transactionConnectorID = commandPayload.connectorId ? commandPayload.connectorId : '1';
1184 if (this._getAuthorizeRemoteTxRequests() && this._getLocalAuthListEnabled() && this.hasAuthorizedTags()) {
1185 // Check if authorized
1186 if (this._authorizedTags.find((value) => value === commandPayload.idTag)) {
1187 // Authorization successful start transaction
1188 this.sendStartTransactionWithTimeout(transactionConnectorID, commandPayload.idTag);
1189 logger.debug(this._logPrefix() + ' Transaction remotely STARTED on ' + this._stationInfo.name + '#' + transactionConnectorID + ' for idTag ' + commandPayload.idTag);
1190 return Constants.OCPP_RESPONSE_ACCEPTED;
1191 }
1192 logger.error(this._logPrefix() + ' Remote starting transaction REJECTED with status ' + commandPayload.idTagInfo.status + ', idTag ' + commandPayload.idTag);
1193 return Constants.OCPP_RESPONSE_REJECTED;
1194 }
1195 // No local authorization check required => start transaction
1196 this.sendStartTransactionWithTimeout(transactionConnectorID, commandPayload.idTag);
1197 logger.debug(this._logPrefix() + ' Transaction remotely STARTED on ' + this._stationInfo.name + '#' + transactionConnectorID + ' for idTag ' + commandPayload.idTag);
1198 return Constants.OCPP_RESPONSE_ACCEPTED;
1199 }
1200
1201 async handleRequestRemoteStopTransaction(commandPayload) {
1202 for (const connector in this._connectors) {
1203 if (this.getConnector(connector).transactionId === commandPayload.transactionId) {
1204 this.sendStopTransaction(commandPayload.transactionId);
1205 return Constants.OCPP_RESPONSE_ACCEPTED;
1206 }
1207 }
1208 logger.info(this._logPrefix() + ' Try to stop remotely a non existing transaction ' + commandPayload.transactionId);
1209 return Constants.OCPP_RESPONSE_REJECTED;
1210 }
1211 }
1212