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