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