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