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