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