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