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