refactor(simulator): remove duplicated code in utils
[e-mobility-charging-stations-simulator.git] / src / charging-station / ChargingStation.ts
1 // Partial Copyright Jerome Benoit. 2021-2023. All Rights Reserved.
2
3 import crypto from 'node:crypto';
4 import fs from 'node:fs';
5 import path from 'node:path';
6 import { URL } from 'node:url';
7 import { parentPort } from 'node:worker_threads';
8
9 import merge from 'just-merge';
10 import WebSocket, { type RawData } from 'ws';
11
12 import {
13 AuthorizedTagsCache,
14 AutomaticTransactionGenerator,
15 ChargingStationConfigurationUtils,
16 ChargingStationUtils,
17 ChargingStationWorkerBroadcastChannel,
18 MessageChannelUtils,
19 SharedLRUCache,
20 } from './internal';
21 import {
22 // OCPP16IncomingRequestService,
23 OCPP16RequestService,
24 // OCPP16ResponseService,
25 OCPP16ServiceUtils,
26 OCPP20IncomingRequestService,
27 OCPP20RequestService,
28 // OCPP20ResponseService,
29 type OCPPIncomingRequestService,
30 type OCPPRequestService,
31 // OCPPServiceUtils,
32 } from './ocpp';
33 import { OCPP16IncomingRequestService } from './ocpp/1.6/OCPP16IncomingRequestService';
34 import { OCPP16ResponseService } from './ocpp/1.6/OCPP16ResponseService';
35 import { OCPP20ResponseService } from './ocpp/2.0/OCPP20ResponseService';
36 import { OCPPServiceUtils } from './ocpp/OCPPServiceUtils';
37 import { BaseError, OCPPError } from '../exception';
38 import { PerformanceStatistics } from '../performance';
39 import {
40 type AutomaticTransactionGeneratorConfiguration,
41 AvailabilityType,
42 type BootNotificationRequest,
43 type BootNotificationResponse,
44 type CachedRequest,
45 type ChargingStationConfiguration,
46 type ChargingStationInfo,
47 type ChargingStationOcppConfiguration,
48 type ChargingStationTemplate,
49 ConnectorPhaseRotation,
50 ConnectorStatus,
51 ConnectorStatusEnum,
52 CurrentType,
53 type ErrorCallback,
54 type ErrorResponse,
55 ErrorType,
56 FileType,
57 FirmwareStatus,
58 type FirmwareStatusNotificationRequest,
59 type FirmwareStatusNotificationResponse,
60 type FirmwareUpgrade,
61 type HeartbeatRequest,
62 type HeartbeatResponse,
63 type IncomingRequest,
64 type IncomingRequestCommand,
65 type JsonType,
66 MessageType,
67 type MeterValue,
68 MeterValueMeasurand,
69 type MeterValuesRequest,
70 type MeterValuesResponse,
71 OCPPVersion,
72 type OutgoingRequest,
73 PowerUnits,
74 RegistrationStatusEnumType,
75 RequestCommand,
76 type Response,
77 type ResponseCallback,
78 StandardParametersKey,
79 type StatusNotificationRequest,
80 type StatusNotificationResponse,
81 StopTransactionReason,
82 type StopTransactionRequest,
83 type StopTransactionResponse,
84 SupervisionUrlDistribution,
85 SupportedFeatureProfiles,
86 VendorDefaultParametersKey,
87 type WSError,
88 WebSocketCloseEventStatusCode,
89 type WsOptions,
90 } from '../types';
91 import {
92 ACElectricUtils,
93 Configuration,
94 Constants,
95 DCElectricUtils,
96 FileUtils,
97 Utils,
98 logger,
99 } from '../utils';
100
101 export class ChargingStation {
102 public readonly index: number;
103 public readonly templateFile: string;
104 public stationInfo!: ChargingStationInfo;
105 public started: boolean;
106 public starting: boolean;
107 public authorizedTagsCache: AuthorizedTagsCache;
108 public automaticTransactionGenerator!: AutomaticTransactionGenerator | undefined;
109 public ocppConfiguration!: ChargingStationOcppConfiguration | undefined;
110 public wsConnection!: WebSocket | null;
111 public readonly connectors: Map<number, ConnectorStatus>;
112 public readonly requests: Map<string, CachedRequest>;
113 public performanceStatistics!: PerformanceStatistics | undefined;
114 public heartbeatSetInterval!: NodeJS.Timeout;
115 public ocppRequestService!: OCPPRequestService;
116 public bootNotificationRequest!: BootNotificationRequest;
117 public bootNotificationResponse!: BootNotificationResponse | undefined;
118 public powerDivider!: number;
119 private stopping: boolean;
120 private configurationFile!: string;
121 private configurationFileHash!: string;
122 private connectorsConfigurationHash!: string;
123 private ocppIncomingRequestService!: OCPPIncomingRequestService;
124 private readonly messageBuffer: Set<string>;
125 private configuredSupervisionUrl!: URL;
126 private configuredSupervisionUrlIndex!: number;
127 private wsConnectionRestarted: boolean;
128 private autoReconnectRetryCount: number;
129 private templateFileWatcher!: fs.FSWatcher | undefined;
130 private readonly sharedLRUCache: SharedLRUCache;
131 private webSocketPingSetInterval!: NodeJS.Timeout;
132 private readonly chargingStationWorkerBroadcastChannel: ChargingStationWorkerBroadcastChannel;
133
134 constructor(index: number, templateFile: string) {
135 this.started = false;
136 this.starting = false;
137 this.stopping = false;
138 this.wsConnectionRestarted = false;
139 this.autoReconnectRetryCount = 0;
140 this.index = index;
141 this.templateFile = templateFile;
142 this.connectors = new Map<number, ConnectorStatus>();
143 this.requests = new Map<string, CachedRequest>();
144 this.messageBuffer = new Set<string>();
145 this.sharedLRUCache = SharedLRUCache.getInstance();
146 this.authorizedTagsCache = AuthorizedTagsCache.getInstance();
147 this.chargingStationWorkerBroadcastChannel = new ChargingStationWorkerBroadcastChannel(this);
148
149 this.initialize();
150 }
151
152 private get wsConnectionUrl(): URL {
153 return new URL(
154 `${
155 this.getSupervisionUrlOcppConfiguration()
156 ? ChargingStationConfigurationUtils.getConfigurationKey(
157 this,
158 this.getSupervisionUrlOcppKey()
159 )?.value
160 : this.configuredSupervisionUrl.href
161 }/${this.stationInfo.chargingStationId}`
162 );
163 }
164
165 public logPrefix = (): string => {
166 return Utils.logPrefix(
167 ` ${
168 (Utils.isNotEmptyString(this?.stationInfo?.chargingStationId) &&
169 this?.stationInfo?.chargingStationId) ??
170 ChargingStationUtils.getChargingStationId(this.index, this.getTemplateFromFile()) ??
171 ''
172 } |`
173 );
174 };
175
176 public hasAuthorizedTags(): boolean {
177 return Utils.isNotEmptyArray(
178 this.authorizedTagsCache.getAuthorizedTags(
179 ChargingStationUtils.getAuthorizationFile(this.stationInfo)
180 )
181 );
182 }
183
184 public getEnableStatistics(): boolean {
185 return this.stationInfo.enableStatistics ?? false;
186 }
187
188 public getMustAuthorizeAtRemoteStart(): boolean {
189 return this.stationInfo.mustAuthorizeAtRemoteStart ?? true;
190 }
191
192 public getPayloadSchemaValidation(): boolean {
193 return this.stationInfo.payloadSchemaValidation ?? true;
194 }
195
196 public getNumberOfPhases(stationInfo?: ChargingStationInfo): number | undefined {
197 const localStationInfo: ChargingStationInfo = stationInfo ?? this.stationInfo;
198 switch (this.getCurrentOutType(stationInfo)) {
199 case CurrentType.AC:
200 return !Utils.isUndefined(localStationInfo.numberOfPhases)
201 ? localStationInfo.numberOfPhases
202 : 3;
203 case CurrentType.DC:
204 return 0;
205 }
206 }
207
208 public isWebSocketConnectionOpened(): boolean {
209 return this?.wsConnection?.readyState === WebSocket.OPEN;
210 }
211
212 public getRegistrationStatus(): RegistrationStatusEnumType | undefined {
213 return this?.bootNotificationResponse?.status;
214 }
215
216 public isInUnknownState(): boolean {
217 return Utils.isNullOrUndefined(this?.bootNotificationResponse?.status);
218 }
219
220 public isInPendingState(): boolean {
221 return this?.bootNotificationResponse?.status === RegistrationStatusEnumType.PENDING;
222 }
223
224 public isInAcceptedState(): boolean {
225 return this?.bootNotificationResponse?.status === RegistrationStatusEnumType.ACCEPTED;
226 }
227
228 public isInRejectedState(): boolean {
229 return this?.bootNotificationResponse?.status === RegistrationStatusEnumType.REJECTED;
230 }
231
232 public isRegistered(): boolean {
233 return (
234 this.isInUnknownState() === false &&
235 (this.isInAcceptedState() === true || this.isInPendingState() === true)
236 );
237 }
238
239 public isChargingStationAvailable(): boolean {
240 return this.getConnectorStatus(0)?.availability === AvailabilityType.OPERATIVE;
241 }
242
243 public isConnectorAvailable(id: number): boolean {
244 return id > 0 && this.getConnectorStatus(id)?.availability === AvailabilityType.OPERATIVE;
245 }
246
247 public getNumberOfConnectors(): number {
248 return this.connectors.get(0) ? this.connectors.size - 1 : this.connectors.size;
249 }
250
251 public getConnectorStatus(id: number): ConnectorStatus | undefined {
252 return this.connectors.get(id);
253 }
254
255 public getCurrentOutType(stationInfo?: ChargingStationInfo): CurrentType {
256 return (stationInfo ?? this.stationInfo)?.currentOutType ?? CurrentType.AC;
257 }
258
259 public getOcppStrictCompliance(): boolean {
260 return this.stationInfo?.ocppStrictCompliance ?? false;
261 }
262
263 public getVoltageOut(stationInfo?: ChargingStationInfo): number | undefined {
264 const defaultVoltageOut = ChargingStationUtils.getDefaultVoltageOut(
265 this.getCurrentOutType(stationInfo),
266 this.templateFile,
267 this.logPrefix()
268 );
269 const localStationInfo: ChargingStationInfo = stationInfo ?? this.stationInfo;
270 return !Utils.isUndefined(localStationInfo.voltageOut)
271 ? localStationInfo.voltageOut
272 : defaultVoltageOut;
273 }
274
275 public getMaximumPower(stationInfo?: ChargingStationInfo): number {
276 const localStationInfo = stationInfo ?? this.stationInfo;
277 return (localStationInfo['maxPower'] as number) ?? localStationInfo.maximumPower;
278 }
279
280 public getConnectorMaximumAvailablePower(connectorId: number): number {
281 let connectorAmperageLimitationPowerLimit: number;
282 if (
283 !Utils.isNullOrUndefined(this.getAmperageLimitation()) &&
284 this.getAmperageLimitation() < this.stationInfo?.maximumAmperage
285 ) {
286 connectorAmperageLimitationPowerLimit =
287 (this.getCurrentOutType() === CurrentType.AC
288 ? ACElectricUtils.powerTotal(
289 this.getNumberOfPhases(),
290 this.getVoltageOut(),
291 this.getAmperageLimitation() * this.getNumberOfConnectors()
292 )
293 : DCElectricUtils.power(this.getVoltageOut(), this.getAmperageLimitation())) /
294 this.powerDivider;
295 }
296 const connectorMaximumPower = this.getMaximumPower() / this.powerDivider;
297 const connectorChargingProfilesPowerLimit =
298 ChargingStationUtils.getChargingStationConnectorChargingProfilesPowerLimit(this, connectorId);
299 return Math.min(
300 isNaN(connectorMaximumPower) ? Infinity : connectorMaximumPower,
301 isNaN(connectorAmperageLimitationPowerLimit)
302 ? Infinity
303 : connectorAmperageLimitationPowerLimit,
304 isNaN(connectorChargingProfilesPowerLimit) ? Infinity : connectorChargingProfilesPowerLimit
305 );
306 }
307
308 public getTransactionIdTag(transactionId: number): string | undefined {
309 for (const connectorId of this.connectors.keys()) {
310 if (
311 connectorId > 0 &&
312 this.getConnectorStatus(connectorId)?.transactionId === transactionId
313 ) {
314 return this.getConnectorStatus(connectorId)?.transactionIdTag;
315 }
316 }
317 }
318
319 public getOutOfOrderEndMeterValues(): boolean {
320 return this.stationInfo?.outOfOrderEndMeterValues ?? false;
321 }
322
323 public getBeginEndMeterValues(): boolean {
324 return this.stationInfo?.beginEndMeterValues ?? false;
325 }
326
327 public getMeteringPerTransaction(): boolean {
328 return this.stationInfo?.meteringPerTransaction ?? true;
329 }
330
331 public getTransactionDataMeterValues(): boolean {
332 return this.stationInfo?.transactionDataMeterValues ?? false;
333 }
334
335 public getMainVoltageMeterValues(): boolean {
336 return this.stationInfo?.mainVoltageMeterValues ?? true;
337 }
338
339 public getPhaseLineToLineVoltageMeterValues(): boolean {
340 return this.stationInfo?.phaseLineToLineVoltageMeterValues ?? false;
341 }
342
343 public getCustomValueLimitationMeterValues(): boolean {
344 return this.stationInfo?.customValueLimitationMeterValues ?? true;
345 }
346
347 public getConnectorIdByTransactionId(transactionId: number): number | undefined {
348 for (const connectorId of this.connectors.keys()) {
349 if (
350 connectorId > 0 &&
351 this.getConnectorStatus(connectorId)?.transactionId === transactionId
352 ) {
353 return connectorId;
354 }
355 }
356 }
357
358 public getEnergyActiveImportRegisterByTransactionId(
359 transactionId: number,
360 rounded = false
361 ): number {
362 return this.getEnergyActiveImportRegister(
363 this.getConnectorStatus(this.getConnectorIdByTransactionId(transactionId)),
364 rounded
365 );
366 }
367
368 public getEnergyActiveImportRegisterByConnectorId(connectorId: number, rounded = false): number {
369 return this.getEnergyActiveImportRegister(this.getConnectorStatus(connectorId), rounded);
370 }
371
372 public getAuthorizeRemoteTxRequests(): boolean {
373 const authorizeRemoteTxRequests = ChargingStationConfigurationUtils.getConfigurationKey(
374 this,
375 StandardParametersKey.AuthorizeRemoteTxRequests
376 );
377 return authorizeRemoteTxRequests
378 ? Utils.convertToBoolean(authorizeRemoteTxRequests.value)
379 : false;
380 }
381
382 public getLocalAuthListEnabled(): boolean {
383 const localAuthListEnabled = ChargingStationConfigurationUtils.getConfigurationKey(
384 this,
385 StandardParametersKey.LocalAuthListEnabled
386 );
387 return localAuthListEnabled ? Utils.convertToBoolean(localAuthListEnabled.value) : false;
388 }
389
390 public startHeartbeat(): void {
391 if (
392 this.getHeartbeatInterval() &&
393 this.getHeartbeatInterval() > 0 &&
394 !this.heartbeatSetInterval
395 ) {
396 this.heartbeatSetInterval = setInterval(() => {
397 this.ocppRequestService
398 .requestHandler<HeartbeatRequest, HeartbeatResponse>(this, RequestCommand.HEARTBEAT)
399 .catch((error) => {
400 logger.error(
401 `${this.logPrefix()} Error while sending '${RequestCommand.HEARTBEAT}':`,
402 error
403 );
404 });
405 }, this.getHeartbeatInterval());
406 logger.info(
407 `${this.logPrefix()} Heartbeat started every ${Utils.formatDurationMilliSeconds(
408 this.getHeartbeatInterval()
409 )}`
410 );
411 } else if (this.heartbeatSetInterval) {
412 logger.info(
413 `${this.logPrefix()} Heartbeat already started every ${Utils.formatDurationMilliSeconds(
414 this.getHeartbeatInterval()
415 )}`
416 );
417 } else {
418 logger.error(
419 `${this.logPrefix()} Heartbeat interval set to ${
420 this.getHeartbeatInterval()
421 ? Utils.formatDurationMilliSeconds(this.getHeartbeatInterval())
422 : this.getHeartbeatInterval()
423 }, not starting the heartbeat`
424 );
425 }
426 }
427
428 public restartHeartbeat(): void {
429 // Stop heartbeat
430 this.stopHeartbeat();
431 // Start heartbeat
432 this.startHeartbeat();
433 }
434
435 public restartWebSocketPing(): void {
436 // Stop WebSocket ping
437 this.stopWebSocketPing();
438 // Start WebSocket ping
439 this.startWebSocketPing();
440 }
441
442 public startMeterValues(connectorId: number, interval: number): void {
443 if (connectorId === 0) {
444 logger.error(
445 `${this.logPrefix()} Trying to start MeterValues on connector Id ${connectorId.toString()}`
446 );
447 return;
448 }
449 if (!this.getConnectorStatus(connectorId)) {
450 logger.error(
451 `${this.logPrefix()} Trying to start MeterValues on non existing connector Id ${connectorId.toString()}`
452 );
453 return;
454 }
455 if (this.getConnectorStatus(connectorId)?.transactionStarted === false) {
456 logger.error(
457 `${this.logPrefix()} Trying to start MeterValues on connector Id ${connectorId} with no transaction started`
458 );
459 return;
460 } else if (
461 this.getConnectorStatus(connectorId)?.transactionStarted === true &&
462 Utils.isNullOrUndefined(this.getConnectorStatus(connectorId)?.transactionId)
463 ) {
464 logger.error(
465 `${this.logPrefix()} Trying to start MeterValues on connector Id ${connectorId} with no transaction id`
466 );
467 return;
468 }
469 if (interval > 0) {
470 this.getConnectorStatus(connectorId).transactionSetInterval = setInterval(() => {
471 // FIXME: Implement OCPP version agnostic helpers
472 const meterValue: MeterValue = OCPP16ServiceUtils.buildMeterValue(
473 this,
474 connectorId,
475 this.getConnectorStatus(connectorId).transactionId,
476 interval
477 );
478 this.ocppRequestService
479 .requestHandler<MeterValuesRequest, MeterValuesResponse>(
480 this,
481 RequestCommand.METER_VALUES,
482 {
483 connectorId,
484 transactionId: this.getConnectorStatus(connectorId)?.transactionId,
485 meterValue: [meterValue],
486 }
487 )
488 .catch((error) => {
489 logger.error(
490 `${this.logPrefix()} Error while sending '${RequestCommand.METER_VALUES}':`,
491 error
492 );
493 });
494 }, interval);
495 } else {
496 logger.error(
497 `${this.logPrefix()} Charging station ${
498 StandardParametersKey.MeterValueSampleInterval
499 } configuration set to ${
500 interval ? Utils.formatDurationMilliSeconds(interval) : interval
501 }, not sending MeterValues`
502 );
503 }
504 }
505
506 public start(): void {
507 if (this.started === false) {
508 if (this.starting === false) {
509 this.starting = true;
510 if (this.getEnableStatistics() === true) {
511 this.performanceStatistics?.start();
512 }
513 this.openWSConnection();
514 // Monitor charging station template file
515 this.templateFileWatcher = FileUtils.watchJsonFile(
516 this.templateFile,
517 FileType.ChargingStationTemplate,
518 this.logPrefix(),
519 undefined,
520 (event, filename): void => {
521 if (Utils.isNotEmptyString(filename) && event === 'change') {
522 try {
523 logger.debug(
524 `${this.logPrefix()} ${FileType.ChargingStationTemplate} ${
525 this.templateFile
526 } file have changed, reload`
527 );
528 this.sharedLRUCache.deleteChargingStationTemplate(this.stationInfo?.templateHash);
529 // Initialize
530 this.initialize();
531 // Restart the ATG
532 this.stopAutomaticTransactionGenerator();
533 if (
534 this.getAutomaticTransactionGeneratorConfigurationFromTemplate()?.enable === true
535 ) {
536 this.startAutomaticTransactionGenerator();
537 }
538 if (this.getEnableStatistics() === true) {
539 this.performanceStatistics?.restart();
540 } else {
541 this.performanceStatistics?.stop();
542 }
543 // FIXME?: restart heartbeat and WebSocket ping when their interval values have changed
544 } catch (error) {
545 logger.error(
546 `${this.logPrefix()} ${FileType.ChargingStationTemplate} file monitoring error:`,
547 error
548 );
549 }
550 }
551 }
552 );
553 this.started = true;
554 parentPort?.postMessage(MessageChannelUtils.buildStartedMessage(this));
555 this.starting = false;
556 } else {
557 logger.warn(`${this.logPrefix()} Charging station is already starting...`);
558 }
559 } else {
560 logger.warn(`${this.logPrefix()} Charging station is already started...`);
561 }
562 }
563
564 public async stop(reason?: StopTransactionReason): Promise<void> {
565 if (this.started === true) {
566 if (this.stopping === false) {
567 this.stopping = true;
568 await this.stopMessageSequence(reason);
569 this.closeWSConnection();
570 if (this.getEnableStatistics() === true) {
571 this.performanceStatistics?.stop();
572 }
573 this.sharedLRUCache.deleteChargingStationConfiguration(this.configurationFileHash);
574 this.templateFileWatcher?.close();
575 this.sharedLRUCache.deleteChargingStationTemplate(this.stationInfo?.templateHash);
576 this.bootNotificationResponse = undefined;
577 this.started = false;
578 parentPort?.postMessage(MessageChannelUtils.buildStoppedMessage(this));
579 this.stopping = false;
580 } else {
581 logger.warn(`${this.logPrefix()} Charging station is already stopping...`);
582 }
583 } else {
584 logger.warn(`${this.logPrefix()} Charging station is already stopped...`);
585 }
586 }
587
588 public async reset(reason?: StopTransactionReason): Promise<void> {
589 await this.stop(reason);
590 await Utils.sleep(this.stationInfo.resetTime);
591 this.initialize();
592 this.start();
593 }
594
595 public saveOcppConfiguration(): void {
596 if (this.getOcppPersistentConfiguration()) {
597 this.saveConfiguration();
598 }
599 }
600
601 public resetConnectorStatus(connectorId: number): void {
602 this.getConnectorStatus(connectorId).idTagLocalAuthorized = false;
603 this.getConnectorStatus(connectorId).idTagAuthorized = false;
604 this.getConnectorStatus(connectorId).transactionRemoteStarted = false;
605 this.getConnectorStatus(connectorId).transactionStarted = false;
606 delete this.getConnectorStatus(connectorId)?.localAuthorizeIdTag;
607 delete this.getConnectorStatus(connectorId)?.authorizeIdTag;
608 delete this.getConnectorStatus(connectorId)?.transactionId;
609 delete this.getConnectorStatus(connectorId)?.transactionIdTag;
610 this.getConnectorStatus(connectorId).transactionEnergyActiveImportRegisterValue = 0;
611 delete this.getConnectorStatus(connectorId)?.transactionBeginMeterValue;
612 this.stopMeterValues(connectorId);
613 parentPort?.postMessage(MessageChannelUtils.buildUpdatedMessage(this));
614 }
615
616 public hasFeatureProfile(featureProfile: SupportedFeatureProfiles): boolean | undefined {
617 return ChargingStationConfigurationUtils.getConfigurationKey(
618 this,
619 StandardParametersKey.SupportedFeatureProfiles
620 )?.value?.includes(featureProfile);
621 }
622
623 public bufferMessage(message: string): void {
624 this.messageBuffer.add(message);
625 }
626
627 public openWSConnection(
628 options: WsOptions = this.stationInfo?.wsOptions ?? {},
629 params: { closeOpened?: boolean; terminateOpened?: boolean } = {
630 closeOpened: false,
631 terminateOpened: false,
632 }
633 ): void {
634 options.handshakeTimeout = options?.handshakeTimeout ?? this.getConnectionTimeout() * 1000;
635 params.closeOpened = params?.closeOpened ?? false;
636 params.terminateOpened = params?.terminateOpened ?? false;
637 if (this.started === false && this.starting === false) {
638 logger.warn(
639 `${this.logPrefix()} Cannot open OCPP connection to URL ${this.wsConnectionUrl.toString()} on stopped charging station`
640 );
641 return;
642 }
643 if (
644 !Utils.isNullOrUndefined(this.stationInfo.supervisionUser) &&
645 !Utils.isNullOrUndefined(this.stationInfo.supervisionPassword)
646 ) {
647 options.auth = `${this.stationInfo.supervisionUser}:${this.stationInfo.supervisionPassword}`;
648 }
649 if (params?.closeOpened) {
650 this.closeWSConnection();
651 }
652 if (params?.terminateOpened) {
653 this.terminateWSConnection();
654 }
655 const ocppVersion = this.stationInfo.ocppVersion ?? OCPPVersion.VERSION_16;
656 let protocol: string;
657 switch (ocppVersion) {
658 case OCPPVersion.VERSION_16:
659 case OCPPVersion.VERSION_20:
660 case OCPPVersion.VERSION_201:
661 protocol = `ocpp${ocppVersion}`;
662 break;
663 default:
664 this.handleUnsupportedVersion(ocppVersion);
665 break;
666 }
667
668 if (this.isWebSocketConnectionOpened() === true) {
669 logger.warn(
670 `${this.logPrefix()} OCPP connection to URL ${this.wsConnectionUrl.toString()} is already opened`
671 );
672 return;
673 }
674
675 logger.info(
676 `${this.logPrefix()} Open OCPP connection to URL ${this.wsConnectionUrl.toString()}`
677 );
678
679 this.wsConnection = new WebSocket(this.wsConnectionUrl, protocol, options);
680
681 // Handle WebSocket message
682 this.wsConnection.on(
683 'message',
684 this.onMessage.bind(this) as (this: WebSocket, data: RawData, isBinary: boolean) => void
685 );
686 // Handle WebSocket error
687 this.wsConnection.on(
688 'error',
689 this.onError.bind(this) as (this: WebSocket, error: Error) => void
690 );
691 // Handle WebSocket close
692 this.wsConnection.on(
693 'close',
694 this.onClose.bind(this) as (this: WebSocket, code: number, reason: Buffer) => void
695 );
696 // Handle WebSocket open
697 this.wsConnection.on('open', this.onOpen.bind(this) as (this: WebSocket) => void);
698 // Handle WebSocket ping
699 this.wsConnection.on('ping', this.onPing.bind(this) as (this: WebSocket, data: Buffer) => void);
700 // Handle WebSocket pong
701 this.wsConnection.on('pong', this.onPong.bind(this) as (this: WebSocket, data: Buffer) => void);
702 }
703
704 public closeWSConnection(): void {
705 if (this.isWebSocketConnectionOpened() === true) {
706 this.wsConnection?.close();
707 this.wsConnection = null;
708 }
709 }
710
711 public startAutomaticTransactionGenerator(
712 connectorIds?: number[],
713 automaticTransactionGeneratorConfiguration?: AutomaticTransactionGeneratorConfiguration
714 ): void {
715 this.automaticTransactionGenerator = AutomaticTransactionGenerator.getInstance(
716 automaticTransactionGeneratorConfiguration ??
717 this.getAutomaticTransactionGeneratorConfigurationFromTemplate(),
718 this
719 );
720 if (Utils.isNotEmptyArray(connectorIds)) {
721 for (const connectorId of connectorIds) {
722 this.automaticTransactionGenerator?.startConnector(connectorId);
723 }
724 } else {
725 this.automaticTransactionGenerator?.start();
726 }
727 parentPort?.postMessage(MessageChannelUtils.buildUpdatedMessage(this));
728 }
729
730 public stopAutomaticTransactionGenerator(connectorIds?: number[]): void {
731 if (Utils.isNotEmptyArray(connectorIds)) {
732 for (const connectorId of connectorIds) {
733 this.automaticTransactionGenerator?.stopConnector(connectorId);
734 }
735 } else {
736 this.automaticTransactionGenerator?.stop();
737 }
738 parentPort?.postMessage(MessageChannelUtils.buildUpdatedMessage(this));
739 }
740
741 public async stopTransactionOnConnector(
742 connectorId: number,
743 reason = StopTransactionReason.NONE
744 ): Promise<StopTransactionResponse> {
745 const transactionId = this.getConnectorStatus(connectorId)?.transactionId;
746 if (
747 this.getBeginEndMeterValues() === true &&
748 this.getOcppStrictCompliance() === true &&
749 this.getOutOfOrderEndMeterValues() === false
750 ) {
751 // FIXME: Implement OCPP version agnostic helpers
752 const transactionEndMeterValue = OCPP16ServiceUtils.buildTransactionEndMeterValue(
753 this,
754 connectorId,
755 this.getEnergyActiveImportRegisterByTransactionId(transactionId)
756 );
757 await this.ocppRequestService.requestHandler<MeterValuesRequest, MeterValuesResponse>(
758 this,
759 RequestCommand.METER_VALUES,
760 {
761 connectorId,
762 transactionId,
763 meterValue: [transactionEndMeterValue],
764 }
765 );
766 }
767 return this.ocppRequestService.requestHandler<StopTransactionRequest, StopTransactionResponse>(
768 this,
769 RequestCommand.STOP_TRANSACTION,
770 {
771 transactionId,
772 meterStop: this.getEnergyActiveImportRegisterByTransactionId(transactionId, true),
773 reason,
774 }
775 );
776 }
777
778 private flushMessageBuffer(): void {
779 if (this.messageBuffer.size > 0) {
780 this.messageBuffer.forEach((message) => {
781 let beginId: string;
782 let commandName: RequestCommand;
783 const [messageType] = JSON.parse(message) as OutgoingRequest | Response | ErrorResponse;
784 const isRequest = messageType === MessageType.CALL_MESSAGE;
785 if (isRequest) {
786 [, , commandName] = JSON.parse(message) as OutgoingRequest;
787 beginId = PerformanceStatistics.beginMeasure(commandName);
788 }
789 this.wsConnection?.send(message);
790 isRequest && PerformanceStatistics.endMeasure(commandName, beginId);
791 logger.debug(
792 `${this.logPrefix()} >> Buffered ${OCPPServiceUtils.getMessageTypeString(
793 messageType
794 )} payload sent: ${message}`
795 );
796 this.messageBuffer.delete(message);
797 });
798 }
799 }
800
801 private getSupervisionUrlOcppConfiguration(): boolean {
802 return this.stationInfo.supervisionUrlOcppConfiguration ?? false;
803 }
804
805 private getSupervisionUrlOcppKey(): string {
806 return this.stationInfo.supervisionUrlOcppKey ?? VendorDefaultParametersKey.ConnectionUrl;
807 }
808
809 private getTemplateFromFile(): ChargingStationTemplate | undefined {
810 let template: ChargingStationTemplate;
811 try {
812 if (this.sharedLRUCache.hasChargingStationTemplate(this.stationInfo?.templateHash)) {
813 template = this.sharedLRUCache.getChargingStationTemplate(this.stationInfo.templateHash);
814 } else {
815 const measureId = `${FileType.ChargingStationTemplate} read`;
816 const beginId = PerformanceStatistics.beginMeasure(measureId);
817 template = JSON.parse(
818 fs.readFileSync(this.templateFile, 'utf8')
819 ) as ChargingStationTemplate;
820 PerformanceStatistics.endMeasure(measureId, beginId);
821 template.templateHash = crypto
822 .createHash(Constants.DEFAULT_HASH_ALGORITHM)
823 .update(JSON.stringify(template))
824 .digest('hex');
825 this.sharedLRUCache.setChargingStationTemplate(template);
826 }
827 } catch (error) {
828 FileUtils.handleFileException(
829 this.templateFile,
830 FileType.ChargingStationTemplate,
831 error as NodeJS.ErrnoException,
832 this.logPrefix()
833 );
834 }
835 return template;
836 }
837
838 private getStationInfoFromTemplate(): ChargingStationInfo {
839 const stationTemplate: ChargingStationTemplate | undefined = this.getTemplateFromFile();
840 if (Utils.isNullOrUndefined(stationTemplate)) {
841 const errorMsg = `Failed to read charging station template file ${this.templateFile}`;
842 logger.error(`${this.logPrefix()} ${errorMsg}`);
843 throw new BaseError(errorMsg);
844 }
845 if (Utils.isEmptyObject(stationTemplate)) {
846 const errorMsg = `Empty charging station information from template file ${this.templateFile}`;
847 logger.error(`${this.logPrefix()} ${errorMsg}`);
848 throw new BaseError(errorMsg);
849 }
850 // Deprecation template keys section
851 ChargingStationUtils.warnDeprecatedTemplateKey(
852 stationTemplate,
853 'supervisionUrl',
854 this.templateFile,
855 this.logPrefix(),
856 "Use 'supervisionUrls' instead"
857 );
858 ChargingStationUtils.convertDeprecatedTemplateKey(
859 stationTemplate,
860 'supervisionUrl',
861 'supervisionUrls'
862 );
863 const stationInfo: ChargingStationInfo =
864 ChargingStationUtils.stationTemplateToStationInfo(stationTemplate);
865 stationInfo.hashId = ChargingStationUtils.getHashId(this.index, stationTemplate);
866 stationInfo.chargingStationId = ChargingStationUtils.getChargingStationId(
867 this.index,
868 stationTemplate
869 );
870 stationInfo.ocppVersion = stationTemplate?.ocppVersion ?? OCPPVersion.VERSION_16;
871 ChargingStationUtils.createSerialNumber(stationTemplate, stationInfo);
872 if (Utils.isNotEmptyArray(stationTemplate?.power)) {
873 stationTemplate.power = stationTemplate.power as number[];
874 const powerArrayRandomIndex = Math.floor(Utils.secureRandom() * stationTemplate.power.length);
875 stationInfo.maximumPower =
876 stationTemplate?.powerUnit === PowerUnits.KILO_WATT
877 ? stationTemplate.power[powerArrayRandomIndex] * 1000
878 : stationTemplate.power[powerArrayRandomIndex];
879 } else {
880 stationTemplate.power = stationTemplate?.power as number;
881 stationInfo.maximumPower =
882 stationTemplate?.powerUnit === PowerUnits.KILO_WATT
883 ? stationTemplate.power * 1000
884 : stationTemplate.power;
885 }
886 stationInfo.firmwareVersionPattern =
887 stationTemplate?.firmwareVersionPattern ?? Constants.SEMVER_PATTERN;
888 if (
889 Utils.isNotEmptyString(stationInfo.firmwareVersion) &&
890 new RegExp(stationInfo.firmwareVersionPattern).test(stationInfo.firmwareVersion) === false
891 ) {
892 logger.warn(
893 `${this.logPrefix()} Firmware version '${stationInfo.firmwareVersion}' in template file ${
894 this.templateFile
895 } does not match firmware version pattern '${stationInfo.firmwareVersionPattern}'`
896 );
897 }
898 stationInfo.firmwareUpgrade = merge<FirmwareUpgrade>(
899 {
900 versionUpgrade: {
901 step: 1,
902 },
903 reset: true,
904 },
905 stationTemplate?.firmwareUpgrade ?? {}
906 );
907 stationInfo.resetTime = !Utils.isNullOrUndefined(stationTemplate?.resetTime)
908 ? stationTemplate.resetTime * 1000
909 : Constants.CHARGING_STATION_DEFAULT_RESET_TIME;
910 const configuredMaxConnectors =
911 ChargingStationUtils.getConfiguredNumberOfConnectors(stationTemplate);
912 ChargingStationUtils.checkConfiguredMaxConnectors(
913 configuredMaxConnectors,
914 this.templateFile,
915 this.logPrefix()
916 );
917 const templateMaxConnectors =
918 ChargingStationUtils.getTemplateMaxNumberOfConnectors(stationTemplate);
919 ChargingStationUtils.checkTemplateMaxConnectors(
920 templateMaxConnectors,
921 this.templateFile,
922 this.logPrefix()
923 );
924 if (
925 configuredMaxConnectors >
926 (stationTemplate?.Connectors[0] ? templateMaxConnectors - 1 : templateMaxConnectors) &&
927 !stationTemplate?.randomConnectors
928 ) {
929 logger.warn(
930 `${this.logPrefix()} Number of connectors exceeds the number of connector configurations in template ${
931 this.templateFile
932 }, forcing random connector configurations affectation`
933 );
934 stationInfo.randomConnectors = true;
935 }
936 // Build connectors if needed (FIXME: should be factored out)
937 this.initializeConnectors(stationInfo, configuredMaxConnectors, templateMaxConnectors);
938 stationInfo.maximumAmperage = this.getMaximumAmperage(stationInfo);
939 ChargingStationUtils.createStationInfoHash(stationInfo);
940 return stationInfo;
941 }
942
943 private getStationInfoFromFile(): ChargingStationInfo | undefined {
944 let stationInfo: ChargingStationInfo | undefined;
945 this.getStationInfoPersistentConfiguration() &&
946 (stationInfo = this.getConfigurationFromFile()?.stationInfo);
947 stationInfo && ChargingStationUtils.createStationInfoHash(stationInfo);
948 return stationInfo;
949 }
950
951 private getStationInfo(): ChargingStationInfo {
952 const stationInfoFromTemplate: ChargingStationInfo = this.getStationInfoFromTemplate();
953 const stationInfoFromFile: ChargingStationInfo | undefined = this.getStationInfoFromFile();
954 // Priority: charging station info from template > charging station info from configuration file > charging station info attribute
955 if (stationInfoFromFile?.templateHash === stationInfoFromTemplate.templateHash) {
956 if (this.stationInfo?.infoHash === stationInfoFromFile?.infoHash) {
957 return this.stationInfo;
958 }
959 return stationInfoFromFile;
960 }
961 stationInfoFromFile &&
962 ChargingStationUtils.propagateSerialNumber(
963 this.getTemplateFromFile(),
964 stationInfoFromFile,
965 stationInfoFromTemplate
966 );
967 return stationInfoFromTemplate;
968 }
969
970 private saveStationInfo(): void {
971 if (this.getStationInfoPersistentConfiguration()) {
972 this.saveConfiguration();
973 }
974 }
975
976 private getOcppPersistentConfiguration(): boolean {
977 return this.stationInfo?.ocppPersistentConfiguration ?? true;
978 }
979
980 private getStationInfoPersistentConfiguration(): boolean {
981 return this.stationInfo?.stationInfoPersistentConfiguration ?? true;
982 }
983
984 private handleUnsupportedVersion(version: OCPPVersion) {
985 const errMsg = `Unsupported protocol version '${version}' configured in template file ${this.templateFile}`;
986 logger.error(`${this.logPrefix()} ${errMsg}`);
987 throw new BaseError(errMsg);
988 }
989
990 private initialize(): void {
991 this.configurationFile = path.join(
992 path.dirname(this.templateFile.replace('station-templates', 'configurations')),
993 `${ChargingStationUtils.getHashId(this.index, this.getTemplateFromFile())}.json`
994 );
995 this.stationInfo = this.getStationInfo();
996 this.saveStationInfo();
997 // Avoid duplication of connectors related information in RAM
998 this.stationInfo?.Connectors && delete this.stationInfo.Connectors;
999 this.configuredSupervisionUrl = this.getConfiguredSupervisionUrl();
1000 if (this.getEnableStatistics() === true) {
1001 this.performanceStatistics = PerformanceStatistics.getInstance(
1002 this.stationInfo.hashId,
1003 this.stationInfo.chargingStationId,
1004 this.configuredSupervisionUrl
1005 );
1006 }
1007 this.bootNotificationRequest = ChargingStationUtils.createBootNotificationRequest(
1008 this.stationInfo
1009 );
1010 this.powerDivider = this.getPowerDivider();
1011 // OCPP configuration
1012 this.ocppConfiguration = this.getOcppConfiguration();
1013 this.initializeOcppConfiguration();
1014 const ocppVersion = this.stationInfo.ocppVersion ?? OCPPVersion.VERSION_16;
1015 switch (ocppVersion) {
1016 case OCPPVersion.VERSION_16:
1017 this.ocppIncomingRequestService =
1018 OCPP16IncomingRequestService.getInstance<OCPP16IncomingRequestService>();
1019 this.ocppRequestService = OCPP16RequestService.getInstance<OCPP16RequestService>(
1020 OCPP16ResponseService.getInstance<OCPP16ResponseService>()
1021 );
1022 break;
1023 case OCPPVersion.VERSION_20:
1024 case OCPPVersion.VERSION_201:
1025 this.ocppIncomingRequestService =
1026 OCPP20IncomingRequestService.getInstance<OCPP20IncomingRequestService>();
1027 this.ocppRequestService = OCPP20RequestService.getInstance<OCPP20RequestService>(
1028 OCPP20ResponseService.getInstance<OCPP20ResponseService>()
1029 );
1030 break;
1031 default:
1032 this.handleUnsupportedVersion(ocppVersion);
1033 break;
1034 }
1035 if (this.stationInfo?.autoRegister === true) {
1036 this.bootNotificationResponse = {
1037 currentTime: new Date(),
1038 interval: this.getHeartbeatInterval() / 1000,
1039 status: RegistrationStatusEnumType.ACCEPTED,
1040 };
1041 }
1042 if (
1043 this.stationInfo.firmwareStatus === FirmwareStatus.Installing &&
1044 Utils.isNotEmptyString(this.stationInfo.firmwareVersion) &&
1045 Utils.isNotEmptyString(this.stationInfo.firmwareVersionPattern)
1046 ) {
1047 const patternGroup: number | undefined =
1048 this.stationInfo.firmwareUpgrade?.versionUpgrade?.patternGroup ??
1049 this.stationInfo.firmwareVersion?.split('.').length;
1050 const match = this.stationInfo?.firmwareVersion
1051 ?.match(new RegExp(this.stationInfo.firmwareVersionPattern))
1052 ?.slice(1, patternGroup + 1);
1053 const patchLevelIndex = match.length - 1;
1054 match[patchLevelIndex] = (
1055 Utils.convertToInt(match[patchLevelIndex]) +
1056 this.stationInfo.firmwareUpgrade?.versionUpgrade?.step
1057 ).toString();
1058 this.stationInfo.firmwareVersion = match?.join('.');
1059 }
1060 }
1061
1062 private initializeOcppConfiguration(): void {
1063 if (
1064 !ChargingStationConfigurationUtils.getConfigurationKey(
1065 this,
1066 StandardParametersKey.HeartbeatInterval
1067 )
1068 ) {
1069 ChargingStationConfigurationUtils.addConfigurationKey(
1070 this,
1071 StandardParametersKey.HeartbeatInterval,
1072 '0'
1073 );
1074 }
1075 if (
1076 !ChargingStationConfigurationUtils.getConfigurationKey(
1077 this,
1078 StandardParametersKey.HeartBeatInterval
1079 )
1080 ) {
1081 ChargingStationConfigurationUtils.addConfigurationKey(
1082 this,
1083 StandardParametersKey.HeartBeatInterval,
1084 '0',
1085 { visible: false }
1086 );
1087 }
1088 if (
1089 this.getSupervisionUrlOcppConfiguration() &&
1090 !ChargingStationConfigurationUtils.getConfigurationKey(this, this.getSupervisionUrlOcppKey())
1091 ) {
1092 ChargingStationConfigurationUtils.addConfigurationKey(
1093 this,
1094 this.getSupervisionUrlOcppKey(),
1095 this.configuredSupervisionUrl.href,
1096 { reboot: true }
1097 );
1098 } else if (
1099 !this.getSupervisionUrlOcppConfiguration() &&
1100 ChargingStationConfigurationUtils.getConfigurationKey(this, this.getSupervisionUrlOcppKey())
1101 ) {
1102 ChargingStationConfigurationUtils.deleteConfigurationKey(
1103 this,
1104 this.getSupervisionUrlOcppKey(),
1105 { save: false }
1106 );
1107 }
1108 if (
1109 Utils.isNotEmptyString(this.stationInfo?.amperageLimitationOcppKey) &&
1110 !ChargingStationConfigurationUtils.getConfigurationKey(
1111 this,
1112 this.stationInfo.amperageLimitationOcppKey
1113 )
1114 ) {
1115 ChargingStationConfigurationUtils.addConfigurationKey(
1116 this,
1117 this.stationInfo.amperageLimitationOcppKey,
1118 (
1119 this.stationInfo.maximumAmperage *
1120 ChargingStationUtils.getAmperageLimitationUnitDivider(this.stationInfo)
1121 ).toString()
1122 );
1123 }
1124 if (
1125 !ChargingStationConfigurationUtils.getConfigurationKey(
1126 this,
1127 StandardParametersKey.SupportedFeatureProfiles
1128 )
1129 ) {
1130 ChargingStationConfigurationUtils.addConfigurationKey(
1131 this,
1132 StandardParametersKey.SupportedFeatureProfiles,
1133 `${SupportedFeatureProfiles.Core},${SupportedFeatureProfiles.FirmwareManagement},${SupportedFeatureProfiles.LocalAuthListManagement},${SupportedFeatureProfiles.SmartCharging},${SupportedFeatureProfiles.RemoteTrigger}`
1134 );
1135 }
1136 ChargingStationConfigurationUtils.addConfigurationKey(
1137 this,
1138 StandardParametersKey.NumberOfConnectors,
1139 this.getNumberOfConnectors().toString(),
1140 { readonly: true },
1141 { overwrite: true }
1142 );
1143 if (
1144 !ChargingStationConfigurationUtils.getConfigurationKey(
1145 this,
1146 StandardParametersKey.MeterValuesSampledData
1147 )
1148 ) {
1149 ChargingStationConfigurationUtils.addConfigurationKey(
1150 this,
1151 StandardParametersKey.MeterValuesSampledData,
1152 MeterValueMeasurand.ENERGY_ACTIVE_IMPORT_REGISTER
1153 );
1154 }
1155 if (
1156 !ChargingStationConfigurationUtils.getConfigurationKey(
1157 this,
1158 StandardParametersKey.ConnectorPhaseRotation
1159 )
1160 ) {
1161 const connectorPhaseRotation = [];
1162 for (const connectorId of this.connectors.keys()) {
1163 // AC/DC
1164 if (connectorId === 0 && this.getNumberOfPhases() === 0) {
1165 connectorPhaseRotation.push(`${connectorId}.${ConnectorPhaseRotation.RST}`);
1166 } else if (connectorId > 0 && this.getNumberOfPhases() === 0) {
1167 connectorPhaseRotation.push(`${connectorId}.${ConnectorPhaseRotation.NotApplicable}`);
1168 // AC
1169 } else if (connectorId > 0 && this.getNumberOfPhases() === 1) {
1170 connectorPhaseRotation.push(`${connectorId}.${ConnectorPhaseRotation.NotApplicable}`);
1171 } else if (connectorId > 0 && this.getNumberOfPhases() === 3) {
1172 connectorPhaseRotation.push(`${connectorId}.${ConnectorPhaseRotation.RST}`);
1173 }
1174 }
1175 ChargingStationConfigurationUtils.addConfigurationKey(
1176 this,
1177 StandardParametersKey.ConnectorPhaseRotation,
1178 connectorPhaseRotation.toString()
1179 );
1180 }
1181 if (
1182 !ChargingStationConfigurationUtils.getConfigurationKey(
1183 this,
1184 StandardParametersKey.AuthorizeRemoteTxRequests
1185 )
1186 ) {
1187 ChargingStationConfigurationUtils.addConfigurationKey(
1188 this,
1189 StandardParametersKey.AuthorizeRemoteTxRequests,
1190 'true'
1191 );
1192 }
1193 if (
1194 !ChargingStationConfigurationUtils.getConfigurationKey(
1195 this,
1196 StandardParametersKey.LocalAuthListEnabled
1197 ) &&
1198 ChargingStationConfigurationUtils.getConfigurationKey(
1199 this,
1200 StandardParametersKey.SupportedFeatureProfiles
1201 )?.value?.includes(SupportedFeatureProfiles.LocalAuthListManagement)
1202 ) {
1203 ChargingStationConfigurationUtils.addConfigurationKey(
1204 this,
1205 StandardParametersKey.LocalAuthListEnabled,
1206 'false'
1207 );
1208 }
1209 if (
1210 !ChargingStationConfigurationUtils.getConfigurationKey(
1211 this,
1212 StandardParametersKey.ConnectionTimeOut
1213 )
1214 ) {
1215 ChargingStationConfigurationUtils.addConfigurationKey(
1216 this,
1217 StandardParametersKey.ConnectionTimeOut,
1218 Constants.DEFAULT_CONNECTION_TIMEOUT.toString()
1219 );
1220 }
1221 this.saveOcppConfiguration();
1222 }
1223
1224 private initializeConnectors(
1225 stationInfo: ChargingStationInfo,
1226 configuredMaxConnectors: number,
1227 templateMaxConnectors: number
1228 ): void {
1229 if (!stationInfo?.Connectors && this.connectors.size === 0) {
1230 const logMsg = `No already defined connectors and charging station information from template ${this.templateFile} with no connectors configuration defined`;
1231 logger.error(`${this.logPrefix()} ${logMsg}`);
1232 throw new BaseError(logMsg);
1233 }
1234 if (!stationInfo?.Connectors[0]) {
1235 logger.warn(
1236 `${this.logPrefix()} Charging station information from template ${
1237 this.templateFile
1238 } with no connector Id 0 configuration`
1239 );
1240 }
1241 if (stationInfo?.Connectors) {
1242 const connectorsConfigHash = crypto
1243 .createHash(Constants.DEFAULT_HASH_ALGORITHM)
1244 .update(`${JSON.stringify(stationInfo?.Connectors)}${configuredMaxConnectors.toString()}`)
1245 .digest('hex');
1246 const connectorsConfigChanged =
1247 this.connectors?.size !== 0 && this.connectorsConfigurationHash !== connectorsConfigHash;
1248 if (this.connectors?.size === 0 || connectorsConfigChanged) {
1249 connectorsConfigChanged && this.connectors.clear();
1250 this.connectorsConfigurationHash = connectorsConfigHash;
1251 // Add connector Id 0
1252 let lastConnector = '0';
1253 for (lastConnector in stationInfo?.Connectors) {
1254 const connectorStatus = stationInfo?.Connectors[lastConnector];
1255 const lastConnectorId = Utils.convertToInt(lastConnector);
1256 if (
1257 lastConnectorId === 0 &&
1258 this.getUseConnectorId0(stationInfo) === true &&
1259 connectorStatus
1260 ) {
1261 this.checkStationInfoConnectorStatus(lastConnectorId, connectorStatus);
1262 this.connectors.set(
1263 lastConnectorId,
1264 Utils.cloneObject<ConnectorStatus>(connectorStatus)
1265 );
1266 this.getConnectorStatus(lastConnectorId).availability = AvailabilityType.OPERATIVE;
1267 if (Utils.isUndefined(this.getConnectorStatus(lastConnectorId)?.chargingProfiles)) {
1268 this.getConnectorStatus(lastConnectorId).chargingProfiles = [];
1269 }
1270 }
1271 }
1272 // Generate all connectors
1273 if ((stationInfo?.Connectors[0] ? templateMaxConnectors - 1 : templateMaxConnectors) > 0) {
1274 for (let index = 1; index <= configuredMaxConnectors; index++) {
1275 const randConnectorId = stationInfo?.randomConnectors
1276 ? Utils.getRandomInteger(Utils.convertToInt(lastConnector), 1)
1277 : index;
1278 const connectorStatus = stationInfo?.Connectors[randConnectorId.toString()];
1279 this.checkStationInfoConnectorStatus(randConnectorId, connectorStatus);
1280 this.connectors.set(index, Utils.cloneObject<ConnectorStatus>(connectorStatus));
1281 this.getConnectorStatus(index).availability = AvailabilityType.OPERATIVE;
1282 if (Utils.isUndefined(this.getConnectorStatus(index)?.chargingProfiles)) {
1283 this.getConnectorStatus(index).chargingProfiles = [];
1284 }
1285 }
1286 }
1287 }
1288 } else {
1289 logger.warn(
1290 `${this.logPrefix()} Charging station information from template ${
1291 this.templateFile
1292 } with no connectors configuration defined, using already defined connectors`
1293 );
1294 }
1295 // Initialize transaction attributes on connectors
1296 for (const connectorId of this.connectors.keys()) {
1297 if (connectorId > 0 && this.getConnectorStatus(connectorId)?.transactionStarted === true) {
1298 logger.warn(
1299 `${this.logPrefix()} Connector ${connectorId} at initialization has a transaction started: ${
1300 this.getConnectorStatus(connectorId)?.transactionId
1301 }`
1302 );
1303 }
1304 if (
1305 connectorId > 0 &&
1306 (this.getConnectorStatus(connectorId)?.transactionStarted === undefined ||
1307 this.getConnectorStatus(connectorId)?.transactionStarted === null)
1308 ) {
1309 this.initializeConnectorStatus(connectorId);
1310 }
1311 }
1312 }
1313
1314 private checkStationInfoConnectorStatus(
1315 connectorId: number,
1316 connectorStatus: ConnectorStatus
1317 ): void {
1318 if (!Utils.isNullOrUndefined(connectorStatus?.status)) {
1319 logger.warn(
1320 `${this.logPrefix()} Charging station information from template ${
1321 this.templateFile
1322 } with connector ${connectorId} status configuration defined, undefine it`
1323 );
1324 connectorStatus.status = undefined;
1325 }
1326 }
1327
1328 private getConfigurationFromFile(): ChargingStationConfiguration | undefined {
1329 let configuration: ChargingStationConfiguration | undefined;
1330 if (this.configurationFile && fs.existsSync(this.configurationFile)) {
1331 try {
1332 if (this.sharedLRUCache.hasChargingStationConfiguration(this.configurationFileHash)) {
1333 configuration = this.sharedLRUCache.getChargingStationConfiguration(
1334 this.configurationFileHash
1335 );
1336 } else {
1337 const measureId = `${FileType.ChargingStationConfiguration} read`;
1338 const beginId = PerformanceStatistics.beginMeasure(measureId);
1339 configuration = JSON.parse(
1340 fs.readFileSync(this.configurationFile, 'utf8')
1341 ) as ChargingStationConfiguration;
1342 PerformanceStatistics.endMeasure(measureId, beginId);
1343 this.configurationFileHash = configuration.configurationHash;
1344 this.sharedLRUCache.setChargingStationConfiguration(configuration);
1345 }
1346 } catch (error) {
1347 FileUtils.handleFileException(
1348 this.configurationFile,
1349 FileType.ChargingStationConfiguration,
1350 error as NodeJS.ErrnoException,
1351 this.logPrefix()
1352 );
1353 }
1354 }
1355 return configuration;
1356 }
1357
1358 private saveConfiguration(): void {
1359 if (this.configurationFile) {
1360 try {
1361 if (!fs.existsSync(path.dirname(this.configurationFile))) {
1362 fs.mkdirSync(path.dirname(this.configurationFile), { recursive: true });
1363 }
1364 const configurationData: ChargingStationConfiguration =
1365 this.getConfigurationFromFile() ?? {};
1366 this.ocppConfiguration?.configurationKey &&
1367 (configurationData.configurationKey = this.ocppConfiguration.configurationKey);
1368 this.stationInfo && (configurationData.stationInfo = this.stationInfo);
1369 delete configurationData.configurationHash;
1370 const configurationHash = crypto
1371 .createHash(Constants.DEFAULT_HASH_ALGORITHM)
1372 .update(JSON.stringify(configurationData))
1373 .digest('hex');
1374 if (this.configurationFileHash !== configurationHash) {
1375 configurationData.configurationHash = configurationHash;
1376 const measureId = `${FileType.ChargingStationConfiguration} write`;
1377 const beginId = PerformanceStatistics.beginMeasure(measureId);
1378 const fileDescriptor = fs.openSync(this.configurationFile, 'w');
1379 fs.writeFileSync(fileDescriptor, JSON.stringify(configurationData, null, 2), 'utf8');
1380 fs.closeSync(fileDescriptor);
1381 PerformanceStatistics.endMeasure(measureId, beginId);
1382 this.sharedLRUCache.deleteChargingStationConfiguration(this.configurationFileHash);
1383 this.configurationFileHash = configurationHash;
1384 this.sharedLRUCache.setChargingStationConfiguration(configurationData);
1385 } else {
1386 logger.debug(
1387 `${this.logPrefix()} Not saving unchanged charging station configuration file ${
1388 this.configurationFile
1389 }`
1390 );
1391 }
1392 } catch (error) {
1393 FileUtils.handleFileException(
1394 this.configurationFile,
1395 FileType.ChargingStationConfiguration,
1396 error as NodeJS.ErrnoException,
1397 this.logPrefix()
1398 );
1399 }
1400 } else {
1401 logger.error(
1402 `${this.logPrefix()} Trying to save charging station configuration to undefined configuration file`
1403 );
1404 }
1405 }
1406
1407 private getOcppConfigurationFromTemplate(): ChargingStationOcppConfiguration | undefined {
1408 return this.getTemplateFromFile()?.Configuration;
1409 }
1410
1411 private getOcppConfigurationFromFile(): ChargingStationOcppConfiguration | undefined {
1412 let configuration: ChargingStationConfiguration | undefined;
1413 if (this.getOcppPersistentConfiguration() === true) {
1414 const configurationFromFile = this.getConfigurationFromFile();
1415 configuration = configurationFromFile?.configurationKey && configurationFromFile;
1416 }
1417 configuration && delete configuration.stationInfo;
1418 return configuration;
1419 }
1420
1421 private getOcppConfiguration(): ChargingStationOcppConfiguration | undefined {
1422 let ocppConfiguration: ChargingStationOcppConfiguration | undefined =
1423 this.getOcppConfigurationFromFile();
1424 if (!ocppConfiguration) {
1425 ocppConfiguration = this.getOcppConfigurationFromTemplate();
1426 }
1427 return ocppConfiguration;
1428 }
1429
1430 private async onOpen(): Promise<void> {
1431 if (this.isWebSocketConnectionOpened() === true) {
1432 logger.info(
1433 `${this.logPrefix()} Connection to OCPP server through ${this.wsConnectionUrl.toString()} succeeded`
1434 );
1435 if (this.isRegistered() === false) {
1436 // Send BootNotification
1437 let registrationRetryCount = 0;
1438 do {
1439 this.bootNotificationResponse = await this.ocppRequestService.requestHandler<
1440 BootNotificationRequest,
1441 BootNotificationResponse
1442 >(this, RequestCommand.BOOT_NOTIFICATION, this.bootNotificationRequest, {
1443 skipBufferingOnError: true,
1444 });
1445 if (this.isRegistered() === false) {
1446 this.getRegistrationMaxRetries() !== -1 && registrationRetryCount++;
1447 await Utils.sleep(
1448 this?.bootNotificationResponse?.interval
1449 ? this.bootNotificationResponse.interval * 1000
1450 : Constants.OCPP_DEFAULT_BOOT_NOTIFICATION_INTERVAL
1451 );
1452 }
1453 } while (
1454 this.isRegistered() === false &&
1455 (registrationRetryCount <= this.getRegistrationMaxRetries() ||
1456 this.getRegistrationMaxRetries() === -1)
1457 );
1458 }
1459 if (this.isRegistered() === true) {
1460 if (this.isInAcceptedState() === true) {
1461 await this.startMessageSequence();
1462 }
1463 } else {
1464 logger.error(
1465 `${this.logPrefix()} Registration failure: max retries reached (${this.getRegistrationMaxRetries()}) or retry disabled (${this.getRegistrationMaxRetries()})`
1466 );
1467 }
1468 this.wsConnectionRestarted = false;
1469 this.autoReconnectRetryCount = 0;
1470 parentPort?.postMessage(MessageChannelUtils.buildUpdatedMessage(this));
1471 } else {
1472 logger.warn(
1473 `${this.logPrefix()} Connection to OCPP server through ${this.wsConnectionUrl.toString()} failed`
1474 );
1475 }
1476 }
1477
1478 private async onClose(code: number, reason: Buffer): Promise<void> {
1479 switch (code) {
1480 // Normal close
1481 case WebSocketCloseEventStatusCode.CLOSE_NORMAL:
1482 case WebSocketCloseEventStatusCode.CLOSE_NO_STATUS:
1483 logger.info(
1484 `${this.logPrefix()} WebSocket normally closed with status '${Utils.getWebSocketCloseEventStatusString(
1485 code
1486 )}' and reason '${reason.toString()}'`
1487 );
1488 this.autoReconnectRetryCount = 0;
1489 break;
1490 // Abnormal close
1491 default:
1492 logger.error(
1493 `${this.logPrefix()} WebSocket abnormally closed with status '${Utils.getWebSocketCloseEventStatusString(
1494 code
1495 )}' and reason '${reason.toString()}'`
1496 );
1497 this.started === true && (await this.reconnect());
1498 break;
1499 }
1500 parentPort?.postMessage(MessageChannelUtils.buildUpdatedMessage(this));
1501 }
1502
1503 private async onMessage(data: RawData): Promise<void> {
1504 let messageType: number;
1505 let messageId: string;
1506 let commandName: IncomingRequestCommand;
1507 let commandPayload: JsonType;
1508 let errorType: ErrorType;
1509 let errorMessage: string;
1510 let errorDetails: JsonType;
1511 let responseCallback: ResponseCallback;
1512 let errorCallback: ErrorCallback;
1513 let requestCommandName: RequestCommand | IncomingRequestCommand;
1514 let requestPayload: JsonType;
1515 let cachedRequest: CachedRequest;
1516 let errMsg: string;
1517 try {
1518 const request = JSON.parse(data.toString()) as IncomingRequest | Response | ErrorResponse;
1519 if (Array.isArray(request) === true) {
1520 [messageType, messageId] = request;
1521 // Check the type of message
1522 switch (messageType) {
1523 // Incoming Message
1524 case MessageType.CALL_MESSAGE:
1525 [, , commandName, commandPayload] = request as IncomingRequest;
1526 if (this.getEnableStatistics() === true) {
1527 this.performanceStatistics?.addRequestStatistic(commandName, messageType);
1528 }
1529 logger.debug(
1530 `${this.logPrefix()} << Command '${commandName}' received request payload: ${JSON.stringify(
1531 request
1532 )}`
1533 );
1534 // Process the message
1535 await this.ocppIncomingRequestService.incomingRequestHandler(
1536 this,
1537 messageId,
1538 commandName,
1539 commandPayload
1540 );
1541 break;
1542 // Outcome Message
1543 case MessageType.CALL_RESULT_MESSAGE:
1544 [, , commandPayload] = request as Response;
1545 if (this.requests.has(messageId) === false) {
1546 // Error
1547 throw new OCPPError(
1548 ErrorType.INTERNAL_ERROR,
1549 `Response for unknown message id ${messageId}`,
1550 undefined,
1551 commandPayload
1552 );
1553 }
1554 // Respond
1555 cachedRequest = this.requests.get(messageId);
1556 if (Array.isArray(cachedRequest) === true) {
1557 [responseCallback, errorCallback, requestCommandName, requestPayload] = cachedRequest;
1558 } else {
1559 throw new OCPPError(
1560 ErrorType.PROTOCOL_ERROR,
1561 `Cached request for message id ${messageId} response is not an array`,
1562 undefined,
1563 cachedRequest as unknown as JsonType
1564 );
1565 }
1566 logger.debug(
1567 `${this.logPrefix()} << Command '${
1568 requestCommandName ?? Constants.UNKNOWN_COMMAND
1569 }' received response payload: ${JSON.stringify(request)}`
1570 );
1571 responseCallback(commandPayload, requestPayload);
1572 break;
1573 // Error Message
1574 case MessageType.CALL_ERROR_MESSAGE:
1575 [, , errorType, errorMessage, errorDetails] = request as ErrorResponse;
1576 if (this.requests.has(messageId) === false) {
1577 // Error
1578 throw new OCPPError(
1579 ErrorType.INTERNAL_ERROR,
1580 `Error response for unknown message id ${messageId}`,
1581 undefined,
1582 { errorType, errorMessage, errorDetails }
1583 );
1584 }
1585 cachedRequest = this.requests.get(messageId);
1586 if (Array.isArray(cachedRequest) === true) {
1587 [, errorCallback, requestCommandName] = cachedRequest;
1588 } else {
1589 throw new OCPPError(
1590 ErrorType.PROTOCOL_ERROR,
1591 `Cached request for message id ${messageId} error response is not an array`,
1592 undefined,
1593 cachedRequest as unknown as JsonType
1594 );
1595 }
1596 logger.debug(
1597 `${this.logPrefix()} << Command '${
1598 requestCommandName ?? Constants.UNKNOWN_COMMAND
1599 }' received error response payload: ${JSON.stringify(request)}`
1600 );
1601 errorCallback(new OCPPError(errorType, errorMessage, requestCommandName, errorDetails));
1602 break;
1603 // Error
1604 default:
1605 // eslint-disable-next-line @typescript-eslint/restrict-template-expressions
1606 errMsg = `Wrong message type ${messageType}`;
1607 logger.error(`${this.logPrefix()} ${errMsg}`);
1608 throw new OCPPError(ErrorType.PROTOCOL_ERROR, errMsg);
1609 }
1610 parentPort?.postMessage(MessageChannelUtils.buildUpdatedMessage(this));
1611 } else {
1612 throw new OCPPError(ErrorType.PROTOCOL_ERROR, 'Incoming message is not an array', null, {
1613 request,
1614 });
1615 }
1616 } catch (error) {
1617 // Log
1618 logger.error(
1619 `${this.logPrefix()} Incoming OCPP command '${
1620 commandName ?? requestCommandName ?? Constants.UNKNOWN_COMMAND
1621 }' message '${data.toString()}'${
1622 messageType !== MessageType.CALL_MESSAGE
1623 ? ` matching cached request '${JSON.stringify(this.requests.get(messageId))}'`
1624 : ''
1625 } processing error:`,
1626 error
1627 );
1628 if (error instanceof OCPPError === false) {
1629 logger.warn(
1630 `${this.logPrefix()} Error thrown at incoming OCPP command '${
1631 commandName ?? requestCommandName ?? Constants.UNKNOWN_COMMAND
1632 }' message '${data.toString()}' handling is not an OCPPError:`,
1633 error
1634 );
1635 }
1636 switch (messageType) {
1637 case MessageType.CALL_MESSAGE:
1638 // Send error
1639 await this.ocppRequestService.sendError(
1640 this,
1641 messageId,
1642 error as OCPPError,
1643 commandName ?? requestCommandName ?? null
1644 );
1645 break;
1646 case MessageType.CALL_RESULT_MESSAGE:
1647 case MessageType.CALL_ERROR_MESSAGE:
1648 if (errorCallback) {
1649 // Reject the deferred promise in case of error at response handling (rejecting an already fulfilled promise is a no-op)
1650 errorCallback(error as OCPPError, false);
1651 } else {
1652 // Remove the request from the cache in case of error at response handling
1653 this.requests.delete(messageId);
1654 }
1655 break;
1656 }
1657 }
1658 }
1659
1660 private onPing(): void {
1661 logger.debug(`${this.logPrefix()} Received a WS ping (rfc6455) from the server`);
1662 }
1663
1664 private onPong(): void {
1665 logger.debug(`${this.logPrefix()} Received a WS pong (rfc6455) from the server`);
1666 }
1667
1668 private onError(error: WSError): void {
1669 this.closeWSConnection();
1670 logger.error(`${this.logPrefix()} WebSocket error:`, error);
1671 }
1672
1673 private getEnergyActiveImportRegister(connectorStatus: ConnectorStatus, rounded = false): number {
1674 if (this.getMeteringPerTransaction() === true) {
1675 return (
1676 (rounded === true
1677 ? Math.round(connectorStatus?.transactionEnergyActiveImportRegisterValue)
1678 : connectorStatus?.transactionEnergyActiveImportRegisterValue) ?? 0
1679 );
1680 }
1681 return (
1682 (rounded === true
1683 ? Math.round(connectorStatus?.energyActiveImportRegisterValue)
1684 : connectorStatus?.energyActiveImportRegisterValue) ?? 0
1685 );
1686 }
1687
1688 private getUseConnectorId0(stationInfo?: ChargingStationInfo): boolean {
1689 const localStationInfo = stationInfo ?? this.stationInfo;
1690 return localStationInfo?.useConnectorId0 ?? true;
1691 }
1692
1693 private getNumberOfRunningTransactions(): number {
1694 let trxCount = 0;
1695 for (const connectorId of this.connectors.keys()) {
1696 if (connectorId > 0 && this.getConnectorStatus(connectorId)?.transactionStarted === true) {
1697 trxCount++;
1698 }
1699 }
1700 return trxCount;
1701 }
1702
1703 private async stopRunningTransactions(reason = StopTransactionReason.NONE): Promise<void> {
1704 for (const connectorId of this.connectors.keys()) {
1705 if (connectorId > 0 && this.getConnectorStatus(connectorId)?.transactionStarted === true) {
1706 await this.stopTransactionOnConnector(connectorId, reason);
1707 }
1708 }
1709 }
1710
1711 // 0 for disabling
1712 private getConnectionTimeout(): number {
1713 if (
1714 ChargingStationConfigurationUtils.getConfigurationKey(
1715 this,
1716 StandardParametersKey.ConnectionTimeOut
1717 )
1718 ) {
1719 return (
1720 parseInt(
1721 ChargingStationConfigurationUtils.getConfigurationKey(
1722 this,
1723 StandardParametersKey.ConnectionTimeOut
1724 ).value
1725 ) ?? Constants.DEFAULT_CONNECTION_TIMEOUT
1726 );
1727 }
1728 return Constants.DEFAULT_CONNECTION_TIMEOUT;
1729 }
1730
1731 // -1 for unlimited, 0 for disabling
1732 private getAutoReconnectMaxRetries(): number | undefined {
1733 if (!Utils.isUndefined(this.stationInfo.autoReconnectMaxRetries)) {
1734 return this.stationInfo.autoReconnectMaxRetries;
1735 }
1736 if (!Utils.isUndefined(Configuration.getAutoReconnectMaxRetries())) {
1737 return Configuration.getAutoReconnectMaxRetries();
1738 }
1739 return -1;
1740 }
1741
1742 // 0 for disabling
1743 private getRegistrationMaxRetries(): number | undefined {
1744 if (!Utils.isUndefined(this.stationInfo.registrationMaxRetries)) {
1745 return this.stationInfo.registrationMaxRetries;
1746 }
1747 return -1;
1748 }
1749
1750 private getPowerDivider(): number {
1751 let powerDivider = this.getNumberOfConnectors();
1752 if (this.stationInfo?.powerSharedByConnectors) {
1753 powerDivider = this.getNumberOfRunningTransactions();
1754 }
1755 return powerDivider;
1756 }
1757
1758 private getMaximumAmperage(stationInfo: ChargingStationInfo): number | undefined {
1759 const maximumPower = this.getMaximumPower(stationInfo);
1760 switch (this.getCurrentOutType(stationInfo)) {
1761 case CurrentType.AC:
1762 return ACElectricUtils.amperagePerPhaseFromPower(
1763 this.getNumberOfPhases(stationInfo),
1764 maximumPower / this.getNumberOfConnectors(),
1765 this.getVoltageOut(stationInfo)
1766 );
1767 case CurrentType.DC:
1768 return DCElectricUtils.amperage(maximumPower, this.getVoltageOut(stationInfo));
1769 }
1770 }
1771
1772 private getAmperageLimitation(): number | undefined {
1773 if (
1774 Utils.isNotEmptyString(this.stationInfo?.amperageLimitationOcppKey) &&
1775 ChargingStationConfigurationUtils.getConfigurationKey(
1776 this,
1777 this.stationInfo.amperageLimitationOcppKey
1778 )
1779 ) {
1780 return (
1781 Utils.convertToInt(
1782 ChargingStationConfigurationUtils.getConfigurationKey(
1783 this,
1784 this.stationInfo.amperageLimitationOcppKey
1785 )?.value
1786 ) / ChargingStationUtils.getAmperageLimitationUnitDivider(this.stationInfo)
1787 );
1788 }
1789 }
1790
1791 private async startMessageSequence(): Promise<void> {
1792 if (this.stationInfo?.autoRegister === true) {
1793 await this.ocppRequestService.requestHandler<
1794 BootNotificationRequest,
1795 BootNotificationResponse
1796 >(this, RequestCommand.BOOT_NOTIFICATION, this.bootNotificationRequest, {
1797 skipBufferingOnError: true,
1798 });
1799 }
1800 // Start WebSocket ping
1801 this.startWebSocketPing();
1802 // Start heartbeat
1803 this.startHeartbeat();
1804 // Initialize connectors status
1805 for (const connectorId of this.connectors.keys()) {
1806 let connectorStatus: ConnectorStatusEnum | undefined;
1807 if (connectorId === 0) {
1808 continue;
1809 } else if (
1810 !this.getConnectorStatus(connectorId)?.status &&
1811 (this.isChargingStationAvailable() === false ||
1812 this.isConnectorAvailable(connectorId) === false)
1813 ) {
1814 connectorStatus = ConnectorStatusEnum.UNAVAILABLE;
1815 } else if (
1816 !this.getConnectorStatus(connectorId)?.status &&
1817 this.getConnectorStatus(connectorId)?.bootStatus
1818 ) {
1819 // Set boot status in template at startup
1820 connectorStatus = this.getConnectorStatus(connectorId)?.bootStatus;
1821 } else if (this.getConnectorStatus(connectorId)?.status) {
1822 // Set previous status at startup
1823 connectorStatus = this.getConnectorStatus(connectorId)?.status;
1824 } else {
1825 // Set default status
1826 connectorStatus = ConnectorStatusEnum.AVAILABLE;
1827 }
1828 await this.ocppRequestService.requestHandler<
1829 StatusNotificationRequest,
1830 StatusNotificationResponse
1831 >(
1832 this,
1833 RequestCommand.STATUS_NOTIFICATION,
1834 OCPPServiceUtils.buildStatusNotificationRequest(this, connectorId, connectorStatus)
1835 );
1836 this.getConnectorStatus(connectorId).status = connectorStatus;
1837 }
1838 if (this.stationInfo?.firmwareStatus === FirmwareStatus.Installing) {
1839 await this.ocppRequestService.requestHandler<
1840 FirmwareStatusNotificationRequest,
1841 FirmwareStatusNotificationResponse
1842 >(this, RequestCommand.FIRMWARE_STATUS_NOTIFICATION, {
1843 status: FirmwareStatus.Installed,
1844 });
1845 this.stationInfo.firmwareStatus = FirmwareStatus.Installed;
1846 }
1847
1848 // Start the ATG
1849 if (this.getAutomaticTransactionGeneratorConfigurationFromTemplate()?.enable === true) {
1850 this.startAutomaticTransactionGenerator();
1851 }
1852 this.wsConnectionRestarted === true && this.flushMessageBuffer();
1853 }
1854
1855 private async stopMessageSequence(
1856 reason: StopTransactionReason = StopTransactionReason.NONE
1857 ): Promise<void> {
1858 // Stop WebSocket ping
1859 this.stopWebSocketPing();
1860 // Stop heartbeat
1861 this.stopHeartbeat();
1862 // Stop ongoing transactions
1863 if (this.automaticTransactionGenerator?.started === true) {
1864 this.stopAutomaticTransactionGenerator();
1865 } else {
1866 await this.stopRunningTransactions(reason);
1867 }
1868 for (const connectorId of this.connectors.keys()) {
1869 if (connectorId > 0) {
1870 await this.ocppRequestService.requestHandler<
1871 StatusNotificationRequest,
1872 StatusNotificationResponse
1873 >(
1874 this,
1875 RequestCommand.STATUS_NOTIFICATION,
1876 OCPPServiceUtils.buildStatusNotificationRequest(
1877 this,
1878 connectorId,
1879 ConnectorStatusEnum.UNAVAILABLE
1880 )
1881 );
1882 this.getConnectorStatus(connectorId).status = undefined;
1883 }
1884 }
1885 }
1886
1887 private startWebSocketPing(): void {
1888 const webSocketPingInterval: number = ChargingStationConfigurationUtils.getConfigurationKey(
1889 this,
1890 StandardParametersKey.WebSocketPingInterval
1891 )
1892 ? Utils.convertToInt(
1893 ChargingStationConfigurationUtils.getConfigurationKey(
1894 this,
1895 StandardParametersKey.WebSocketPingInterval
1896 )?.value
1897 )
1898 : 0;
1899 if (webSocketPingInterval > 0 && !this.webSocketPingSetInterval) {
1900 this.webSocketPingSetInterval = setInterval(() => {
1901 if (this.isWebSocketConnectionOpened() === true) {
1902 this.wsConnection?.ping();
1903 }
1904 }, webSocketPingInterval * 1000);
1905 logger.info(
1906 `${this.logPrefix()} WebSocket ping started every ${Utils.formatDurationSeconds(
1907 webSocketPingInterval
1908 )}`
1909 );
1910 } else if (this.webSocketPingSetInterval) {
1911 logger.info(
1912 `${this.logPrefix()} WebSocket ping already started every ${Utils.formatDurationSeconds(
1913 webSocketPingInterval
1914 )}`
1915 );
1916 } else {
1917 logger.error(
1918 `${this.logPrefix()} WebSocket ping interval set to ${
1919 webSocketPingInterval
1920 ? Utils.formatDurationSeconds(webSocketPingInterval)
1921 : webSocketPingInterval
1922 }, not starting the WebSocket ping`
1923 );
1924 }
1925 }
1926
1927 private stopWebSocketPing(): void {
1928 if (this.webSocketPingSetInterval) {
1929 clearInterval(this.webSocketPingSetInterval);
1930 }
1931 }
1932
1933 private getConfiguredSupervisionUrl(): URL {
1934 const supervisionUrls = this.stationInfo?.supervisionUrls ?? Configuration.getSupervisionUrls();
1935 if (Utils.isNotEmptyArray(supervisionUrls)) {
1936 switch (Configuration.getSupervisionUrlDistribution()) {
1937 case SupervisionUrlDistribution.ROUND_ROBIN:
1938 // FIXME
1939 this.configuredSupervisionUrlIndex = (this.index - 1) % supervisionUrls.length;
1940 break;
1941 case SupervisionUrlDistribution.RANDOM:
1942 this.configuredSupervisionUrlIndex = Math.floor(
1943 Utils.secureRandom() * supervisionUrls.length
1944 );
1945 break;
1946 case SupervisionUrlDistribution.CHARGING_STATION_AFFINITY:
1947 this.configuredSupervisionUrlIndex = (this.index - 1) % supervisionUrls.length;
1948 break;
1949 default:
1950 logger.error(
1951 `${this.logPrefix()} Unknown supervision url distribution '${Configuration.getSupervisionUrlDistribution()}' from values '${SupervisionUrlDistribution.toString()}', defaulting to ${
1952 SupervisionUrlDistribution.CHARGING_STATION_AFFINITY
1953 }`
1954 );
1955 this.configuredSupervisionUrlIndex = (this.index - 1) % supervisionUrls.length;
1956 break;
1957 }
1958 return new URL(supervisionUrls[this.configuredSupervisionUrlIndex]);
1959 }
1960 return new URL(supervisionUrls as string);
1961 }
1962
1963 private getHeartbeatInterval(): number {
1964 const HeartbeatInterval = ChargingStationConfigurationUtils.getConfigurationKey(
1965 this,
1966 StandardParametersKey.HeartbeatInterval
1967 );
1968 if (HeartbeatInterval) {
1969 return Utils.convertToInt(HeartbeatInterval.value) * 1000;
1970 }
1971 const HeartBeatInterval = ChargingStationConfigurationUtils.getConfigurationKey(
1972 this,
1973 StandardParametersKey.HeartBeatInterval
1974 );
1975 if (HeartBeatInterval) {
1976 return Utils.convertToInt(HeartBeatInterval.value) * 1000;
1977 }
1978 this.stationInfo?.autoRegister === false &&
1979 logger.warn(
1980 `${this.logPrefix()} Heartbeat interval configuration key not set, using default value: ${
1981 Constants.DEFAULT_HEARTBEAT_INTERVAL
1982 }`
1983 );
1984 return Constants.DEFAULT_HEARTBEAT_INTERVAL;
1985 }
1986
1987 private stopHeartbeat(): void {
1988 if (this.heartbeatSetInterval) {
1989 clearInterval(this.heartbeatSetInterval);
1990 }
1991 }
1992
1993 private terminateWSConnection(): void {
1994 if (this.isWebSocketConnectionOpened() === true) {
1995 this.wsConnection?.terminate();
1996 this.wsConnection = null;
1997 }
1998 }
1999
2000 private stopMeterValues(connectorId: number) {
2001 if (this.getConnectorStatus(connectorId)?.transactionSetInterval) {
2002 clearInterval(this.getConnectorStatus(connectorId)?.transactionSetInterval);
2003 }
2004 }
2005
2006 private getReconnectExponentialDelay(): boolean {
2007 return this.stationInfo?.reconnectExponentialDelay ?? false;
2008 }
2009
2010 private async reconnect(): Promise<void> {
2011 // Stop WebSocket ping
2012 this.stopWebSocketPing();
2013 // Stop heartbeat
2014 this.stopHeartbeat();
2015 // Stop the ATG if needed
2016 if (this.automaticTransactionGenerator?.configuration?.stopOnConnectionFailure === true) {
2017 this.stopAutomaticTransactionGenerator();
2018 }
2019 if (
2020 this.autoReconnectRetryCount < this.getAutoReconnectMaxRetries() ||
2021 this.getAutoReconnectMaxRetries() === -1
2022 ) {
2023 this.autoReconnectRetryCount++;
2024 const reconnectDelay = this.getReconnectExponentialDelay()
2025 ? Utils.exponentialDelay(this.autoReconnectRetryCount)
2026 : this.getConnectionTimeout() * 1000;
2027 const reconnectDelayWithdraw = 1000;
2028 const reconnectTimeout =
2029 reconnectDelay && reconnectDelay - reconnectDelayWithdraw > 0
2030 ? reconnectDelay - reconnectDelayWithdraw
2031 : 0;
2032 logger.error(
2033 `${this.logPrefix()} WebSocket connection retry in ${Utils.roundTo(
2034 reconnectDelay,
2035 2
2036 )}ms, timeout ${reconnectTimeout}ms`
2037 );
2038 await Utils.sleep(reconnectDelay);
2039 logger.error(
2040 `${this.logPrefix()} WebSocket connection retry #${this.autoReconnectRetryCount.toString()}`
2041 );
2042 this.openWSConnection(
2043 { ...(this.stationInfo?.wsOptions ?? {}), handshakeTimeout: reconnectTimeout },
2044 { closeOpened: true }
2045 );
2046 this.wsConnectionRestarted = true;
2047 } else if (this.getAutoReconnectMaxRetries() !== -1) {
2048 logger.error(
2049 `${this.logPrefix()} WebSocket connection retries failure: maximum retries reached (${
2050 this.autoReconnectRetryCount
2051 }) or retries disabled (${this.getAutoReconnectMaxRetries()})`
2052 );
2053 }
2054 }
2055
2056 private getAutomaticTransactionGeneratorConfigurationFromTemplate():
2057 | AutomaticTransactionGeneratorConfiguration
2058 | undefined {
2059 return this.getTemplateFromFile()?.AutomaticTransactionGenerator;
2060 }
2061
2062 private initializeConnectorStatus(connectorId: number): void {
2063 this.getConnectorStatus(connectorId).idTagLocalAuthorized = false;
2064 this.getConnectorStatus(connectorId).idTagAuthorized = false;
2065 this.getConnectorStatus(connectorId).transactionRemoteStarted = false;
2066 this.getConnectorStatus(connectorId).transactionStarted = false;
2067 this.getConnectorStatus(connectorId).energyActiveImportRegisterValue = 0;
2068 this.getConnectorStatus(connectorId).transactionEnergyActiveImportRegisterValue = 0;
2069 }
2070 }