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