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