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