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