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