bd3fb81283b079f66ceed55b380434666c331613
[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): Promise<void> {
728 if (this.started === true) {
729 if (this.stopping === false) {
730 this.stopping = true;
731 await this.stopMessageSequence(reason);
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 async stopAutomaticTransactionGenerator(connectorIds?: number[]): Promise<void> {
888 if (isNotEmptyArray(connectorIds)) {
889 for (const connectorId of connectorIds!) {
890 await this.automaticTransactionGenerator?.stopConnector(connectorId);
891 }
892 } else {
893 await this.automaticTransactionGenerator?.stop();
894 }
895 this.saveAutomaticTransactionGeneratorConfiguration();
896 parentPort?.postMessage(buildUpdatedMessage(this));
897 }
898
899 public async stopTransactionOnConnector(
900 connectorId: number,
901 reason = StopTransactionReason.NONE,
902 ): Promise<StopTransactionResponse> {
903 const transactionId = this.getConnectorStatus(connectorId)?.transactionId;
904 if (
905 this.getBeginEndMeterValues() === true &&
906 this.getOcppStrictCompliance() === true &&
907 this.getOutOfOrderEndMeterValues() === false
908 ) {
909 // FIXME: Implement OCPP version agnostic helpers
910 const transactionEndMeterValue = OCPP16ServiceUtils.buildTransactionEndMeterValue(
911 this,
912 connectorId,
913 this.getEnergyActiveImportRegisterByTransactionId(transactionId!),
914 );
915 await this.ocppRequestService.requestHandler<MeterValuesRequest, MeterValuesResponse>(
916 this,
917 RequestCommand.METER_VALUES,
918 {
919 connectorId,
920 transactionId,
921 meterValue: [transactionEndMeterValue],
922 },
923 );
924 }
925 return this.ocppRequestService.requestHandler<StopTransactionRequest, StopTransactionResponse>(
926 this,
927 RequestCommand.STOP_TRANSACTION,
928 {
929 transactionId,
930 meterStop: this.getEnergyActiveImportRegisterByTransactionId(transactionId!, true),
931 reason,
932 },
933 );
934 }
935
936 public 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 } as ChargingStationConfiguration),
1705 )
1706 .digest('hex');
1707 if (this.configurationFileHash !== configurationHash) {
1708 AsyncLock.runExclusive(AsyncLockType.configuration, () => {
1709 configurationData.configurationHash = configurationHash;
1710 const measureId = `${FileType.ChargingStationConfiguration} write`;
1711 const beginId = PerformanceStatistics.beginMeasure(measureId);
1712 writeFileSync(
1713 this.configurationFile,
1714 JSON.stringify(configurationData, undefined, 2),
1715 'utf8',
1716 );
1717 PerformanceStatistics.endMeasure(measureId, beginId);
1718 this.sharedLRUCache.deleteChargingStationConfiguration(this.configurationFileHash);
1719 this.sharedLRUCache.setChargingStationConfiguration(configurationData);
1720 this.configurationFileHash = configurationHash;
1721 }).catch((error) => {
1722 handleFileException(
1723 this.configurationFile,
1724 FileType.ChargingStationConfiguration,
1725 error as NodeJS.ErrnoException,
1726 this.logPrefix(),
1727 );
1728 });
1729 } else {
1730 logger.debug(
1731 `${this.logPrefix()} Not saving unchanged charging station configuration file ${
1732 this.configurationFile
1733 }`,
1734 );
1735 }
1736 } catch (error) {
1737 handleFileException(
1738 this.configurationFile,
1739 FileType.ChargingStationConfiguration,
1740 error as NodeJS.ErrnoException,
1741 this.logPrefix(),
1742 );
1743 }
1744 } else {
1745 logger.error(
1746 `${this.logPrefix()} Trying to save charging station configuration to undefined configuration file`,
1747 );
1748 }
1749 }
1750
1751 private getOcppConfigurationFromTemplate(): ChargingStationOcppConfiguration | undefined {
1752 return this.getTemplateFromFile()?.Configuration;
1753 }
1754
1755 private getOcppConfigurationFromFile(): ChargingStationOcppConfiguration | undefined {
1756 const configurationKey = this.getConfigurationFromFile()?.configurationKey;
1757 if (this.getOcppPersistentConfiguration() === true && configurationKey) {
1758 return { configurationKey };
1759 }
1760 return undefined;
1761 }
1762
1763 private getOcppConfiguration(): ChargingStationOcppConfiguration | undefined {
1764 let ocppConfiguration: ChargingStationOcppConfiguration | undefined =
1765 this.getOcppConfigurationFromFile();
1766 if (!ocppConfiguration) {
1767 ocppConfiguration = this.getOcppConfigurationFromTemplate();
1768 }
1769 return ocppConfiguration;
1770 }
1771
1772 private async onOpen(): Promise<void> {
1773 if (this.isWebSocketConnectionOpened() === true) {
1774 logger.info(
1775 `${this.logPrefix()} Connection to OCPP server through ${this.wsConnectionUrl.toString()} succeeded`,
1776 );
1777 if (this.isRegistered() === false) {
1778 // Send BootNotification
1779 let registrationRetryCount = 0;
1780 do {
1781 this.bootNotificationResponse = await this.ocppRequestService.requestHandler<
1782 BootNotificationRequest,
1783 BootNotificationResponse
1784 >(this, RequestCommand.BOOT_NOTIFICATION, this.bootNotificationRequest, {
1785 skipBufferingOnError: true,
1786 });
1787 if (this.isRegistered() === false) {
1788 this.getRegistrationMaxRetries() !== -1 && ++registrationRetryCount;
1789 await sleep(
1790 this?.bootNotificationResponse?.interval
1791 ? secondsToMilliseconds(this.bootNotificationResponse.interval)
1792 : Constants.DEFAULT_BOOT_NOTIFICATION_INTERVAL,
1793 );
1794 }
1795 } while (
1796 this.isRegistered() === false &&
1797 (registrationRetryCount <= this.getRegistrationMaxRetries()! ||
1798 this.getRegistrationMaxRetries() === -1)
1799 );
1800 }
1801 if (this.isRegistered() === true) {
1802 if (this.inAcceptedState() === true) {
1803 await this.startMessageSequence();
1804 }
1805 } else {
1806 logger.error(
1807 `${this.logPrefix()} Registration failure: max retries reached (${this.getRegistrationMaxRetries()}) or retry disabled (${this.getRegistrationMaxRetries()})`,
1808 );
1809 }
1810 this.wsConnectionRestarted = false;
1811 this.autoReconnectRetryCount = 0;
1812 parentPort?.postMessage(buildUpdatedMessage(this));
1813 } else {
1814 logger.warn(
1815 `${this.logPrefix()} Connection to OCPP server through ${this.wsConnectionUrl.toString()} failed`,
1816 );
1817 }
1818 }
1819
1820 private async onClose(code: number, reason: Buffer): Promise<void> {
1821 switch (code) {
1822 // Normal close
1823 case WebSocketCloseEventStatusCode.CLOSE_NORMAL:
1824 case WebSocketCloseEventStatusCode.CLOSE_NO_STATUS:
1825 logger.info(
1826 `${this.logPrefix()} WebSocket normally closed with status '${getWebSocketCloseEventStatusString(
1827 code,
1828 )}' and reason '${reason.toString()}'`,
1829 );
1830 this.autoReconnectRetryCount = 0;
1831 break;
1832 // Abnormal close
1833 default:
1834 logger.error(
1835 `${this.logPrefix()} WebSocket abnormally closed with status '${getWebSocketCloseEventStatusString(
1836 code,
1837 )}' and reason '${reason.toString()}'`,
1838 );
1839 this.started === true && (await this.reconnect());
1840 break;
1841 }
1842 parentPort?.postMessage(buildUpdatedMessage(this));
1843 }
1844
1845 private getCachedRequest(messageType: MessageType, messageId: string): CachedRequest | undefined {
1846 const cachedRequest = this.requests.get(messageId);
1847 if (Array.isArray(cachedRequest) === true) {
1848 return cachedRequest;
1849 }
1850 throw new OCPPError(
1851 ErrorType.PROTOCOL_ERROR,
1852 `Cached request for message id ${messageId} ${OCPPServiceUtils.getMessageTypeString(
1853 messageType,
1854 )} is not an array`,
1855 undefined,
1856 cachedRequest as JsonType,
1857 );
1858 }
1859
1860 private async handleIncomingMessage(request: IncomingRequest): Promise<void> {
1861 const [messageType, messageId, commandName, commandPayload] = request;
1862 if (this.getEnableStatistics() === true) {
1863 this.performanceStatistics?.addRequestStatistic(commandName, messageType);
1864 }
1865 logger.debug(
1866 `${this.logPrefix()} << Command '${commandName}' received request payload: ${JSON.stringify(
1867 request,
1868 )}`,
1869 );
1870 // Process the message
1871 await this.ocppIncomingRequestService.incomingRequestHandler(
1872 this,
1873 messageId,
1874 commandName,
1875 commandPayload,
1876 );
1877 }
1878
1879 private handleResponseMessage(response: Response): void {
1880 const [messageType, messageId, commandPayload] = response;
1881 if (this.requests.has(messageId) === false) {
1882 // Error
1883 throw new OCPPError(
1884 ErrorType.INTERNAL_ERROR,
1885 `Response for unknown message id ${messageId}`,
1886 undefined,
1887 commandPayload,
1888 );
1889 }
1890 // Respond
1891 const [responseCallback, , requestCommandName, requestPayload] = this.getCachedRequest(
1892 messageType,
1893 messageId,
1894 )!;
1895 logger.debug(
1896 `${this.logPrefix()} << Command '${
1897 requestCommandName ?? Constants.UNKNOWN_COMMAND
1898 }' received response payload: ${JSON.stringify(response)}`,
1899 );
1900 responseCallback(commandPayload, requestPayload);
1901 }
1902
1903 private handleErrorMessage(errorResponse: ErrorResponse): void {
1904 const [messageType, messageId, errorType, errorMessage, errorDetails] = errorResponse;
1905 if (this.requests.has(messageId) === false) {
1906 // Error
1907 throw new OCPPError(
1908 ErrorType.INTERNAL_ERROR,
1909 `Error response for unknown message id ${messageId}`,
1910 undefined,
1911 { errorType, errorMessage, errorDetails },
1912 );
1913 }
1914 const [, errorCallback, requestCommandName] = this.getCachedRequest(messageType, messageId)!;
1915 logger.debug(
1916 `${this.logPrefix()} << Command '${
1917 requestCommandName ?? Constants.UNKNOWN_COMMAND
1918 }' received error response payload: ${JSON.stringify(errorResponse)}`,
1919 );
1920 errorCallback(new OCPPError(errorType, errorMessage, requestCommandName, errorDetails));
1921 }
1922
1923 private async onMessage(data: RawData): Promise<void> {
1924 let request: IncomingRequest | Response | ErrorResponse | undefined;
1925 let messageType: number | undefined;
1926 let errorMsg: string;
1927 try {
1928 // eslint-disable-next-line @typescript-eslint/no-base-to-string
1929 request = JSON.parse(data.toString()) as IncomingRequest | Response | ErrorResponse;
1930 if (Array.isArray(request) === true) {
1931 [messageType] = request;
1932 // Check the type of message
1933 switch (messageType) {
1934 // Incoming Message
1935 case MessageType.CALL_MESSAGE:
1936 await this.handleIncomingMessage(request as IncomingRequest);
1937 break;
1938 // Response Message
1939 case MessageType.CALL_RESULT_MESSAGE:
1940 this.handleResponseMessage(request as Response);
1941 break;
1942 // Error Message
1943 case MessageType.CALL_ERROR_MESSAGE:
1944 this.handleErrorMessage(request as ErrorResponse);
1945 break;
1946 // Unknown Message
1947 default:
1948 // eslint-disable-next-line @typescript-eslint/restrict-template-expressions
1949 errorMsg = `Wrong message type ${messageType}`;
1950 logger.error(`${this.logPrefix()} ${errorMsg}`);
1951 throw new OCPPError(ErrorType.PROTOCOL_ERROR, errorMsg);
1952 }
1953 parentPort?.postMessage(buildUpdatedMessage(this));
1954 } else {
1955 throw new OCPPError(
1956 ErrorType.PROTOCOL_ERROR,
1957 'Incoming message is not an array',
1958 undefined,
1959 {
1960 request,
1961 },
1962 );
1963 }
1964 } catch (error) {
1965 let commandName: IncomingRequestCommand | undefined;
1966 let requestCommandName: RequestCommand | IncomingRequestCommand | undefined;
1967 let errorCallback: ErrorCallback;
1968 const [, messageId] = request!;
1969 switch (messageType) {
1970 case MessageType.CALL_MESSAGE:
1971 [, , commandName] = request as IncomingRequest;
1972 // Send error
1973 await this.ocppRequestService.sendError(this, messageId, error as OCPPError, commandName);
1974 break;
1975 case MessageType.CALL_RESULT_MESSAGE:
1976 case MessageType.CALL_ERROR_MESSAGE:
1977 if (this.requests.has(messageId) === true) {
1978 [, errorCallback, requestCommandName] = this.getCachedRequest(messageType, messageId)!;
1979 // Reject the deferred promise in case of error at response handling (rejecting an already fulfilled promise is a no-op)
1980 errorCallback(error as OCPPError, false);
1981 } else {
1982 // Remove the request from the cache in case of error at response handling
1983 this.requests.delete(messageId);
1984 }
1985 break;
1986 }
1987 if (error instanceof OCPPError === false) {
1988 logger.warn(
1989 `${this.logPrefix()} Error thrown at incoming OCPP command '${
1990 commandName ?? requestCommandName ?? Constants.UNKNOWN_COMMAND
1991 // eslint-disable-next-line @typescript-eslint/no-base-to-string
1992 }' message '${data.toString()}' handling is not an OCPPError:`,
1993 error,
1994 );
1995 }
1996 logger.error(
1997 `${this.logPrefix()} Incoming OCPP command '${
1998 commandName ?? requestCommandName ?? Constants.UNKNOWN_COMMAND
1999 // eslint-disable-next-line @typescript-eslint/no-base-to-string
2000 }' message '${data.toString()}'${
2001 messageType !== MessageType.CALL_MESSAGE
2002 ? ` matching cached request '${JSON.stringify(this.requests.get(messageId))}'`
2003 : ''
2004 } processing error:`,
2005 error,
2006 );
2007 }
2008 }
2009
2010 private onPing(): void {
2011 logger.debug(`${this.logPrefix()} Received a WS ping (rfc6455) from the server`);
2012 }
2013
2014 private onPong(): void {
2015 logger.debug(`${this.logPrefix()} Received a WS pong (rfc6455) from the server`);
2016 }
2017
2018 private onError(error: WSError): void {
2019 this.closeWSConnection();
2020 logger.error(`${this.logPrefix()} WebSocket error:`, error);
2021 }
2022
2023 private getEnergyActiveImportRegister(connectorStatus: ConnectorStatus, rounded = false): number {
2024 if (this.getMeteringPerTransaction() === true) {
2025 return (
2026 (rounded === true
2027 ? Math.round(connectorStatus.transactionEnergyActiveImportRegisterValue!)
2028 : connectorStatus?.transactionEnergyActiveImportRegisterValue) ?? 0
2029 );
2030 }
2031 return (
2032 (rounded === true
2033 ? Math.round(connectorStatus.energyActiveImportRegisterValue!)
2034 : connectorStatus?.energyActiveImportRegisterValue) ?? 0
2035 );
2036 }
2037
2038 private getUseConnectorId0(stationTemplate?: ChargingStationTemplate): boolean {
2039 return stationTemplate?.useConnectorId0 ?? true;
2040 }
2041
2042 private async stopRunningTransactions(reason = StopTransactionReason.NONE): Promise<void> {
2043 if (this.hasEvses) {
2044 for (const [evseId, evseStatus] of this.evses) {
2045 if (evseId === 0) {
2046 continue;
2047 }
2048 for (const [connectorId, connectorStatus] of evseStatus.connectors) {
2049 if (connectorStatus.transactionStarted === true) {
2050 await this.stopTransactionOnConnector(connectorId, reason);
2051 }
2052 }
2053 }
2054 } else {
2055 for (const connectorId of this.connectors.keys()) {
2056 if (connectorId > 0 && this.getConnectorStatus(connectorId)?.transactionStarted === true) {
2057 await this.stopTransactionOnConnector(connectorId, reason);
2058 }
2059 }
2060 }
2061 }
2062
2063 // 0 for disabling
2064 private getConnectionTimeout(): number {
2065 if (getConfigurationKey(this, StandardParametersKey.ConnectionTimeOut)) {
2066 return (
2067 parseInt(getConfigurationKey(this, StandardParametersKey.ConnectionTimeOut)!.value!) ??
2068 Constants.DEFAULT_CONNECTION_TIMEOUT
2069 );
2070 }
2071 return Constants.DEFAULT_CONNECTION_TIMEOUT;
2072 }
2073
2074 // -1 for unlimited, 0 for disabling
2075 private getAutoReconnectMaxRetries(): number | undefined {
2076 return this.stationInfo.autoReconnectMaxRetries ?? -1;
2077 }
2078
2079 // -1 for unlimited, 0 for disabling
2080 private getRegistrationMaxRetries(): number | undefined {
2081 return this.stationInfo.registrationMaxRetries ?? -1;
2082 }
2083
2084 private getPowerDivider(): number {
2085 let powerDivider = this.hasEvses ? this.getNumberOfEvses() : this.getNumberOfConnectors();
2086 if (this.stationInfo?.powerSharedByConnectors) {
2087 powerDivider = this.getNumberOfRunningTransactions();
2088 }
2089 return powerDivider;
2090 }
2091
2092 private getMaximumAmperage(stationInfo: ChargingStationInfo): number | undefined {
2093 const maximumPower = this.getMaximumPower(stationInfo);
2094 switch (this.getCurrentOutType(stationInfo)) {
2095 case CurrentType.AC:
2096 return ACElectricUtils.amperagePerPhaseFromPower(
2097 this.getNumberOfPhases(stationInfo),
2098 maximumPower / (this.hasEvses ? this.getNumberOfEvses() : this.getNumberOfConnectors()),
2099 this.getVoltageOut(stationInfo),
2100 );
2101 case CurrentType.DC:
2102 return DCElectricUtils.amperage(maximumPower, this.getVoltageOut(stationInfo));
2103 }
2104 }
2105
2106 private getAmperageLimitation(): number | undefined {
2107 if (
2108 isNotEmptyString(this.stationInfo?.amperageLimitationOcppKey) &&
2109 getConfigurationKey(this, this.stationInfo.amperageLimitationOcppKey!)
2110 ) {
2111 return (
2112 convertToInt(
2113 getConfigurationKey(this, this.stationInfo.amperageLimitationOcppKey!)?.value,
2114 ) / getAmperageLimitationUnitDivider(this.stationInfo)
2115 );
2116 }
2117 }
2118
2119 private async startMessageSequence(): Promise<void> {
2120 if (this.stationInfo?.autoRegister === true) {
2121 await this.ocppRequestService.requestHandler<
2122 BootNotificationRequest,
2123 BootNotificationResponse
2124 >(this, RequestCommand.BOOT_NOTIFICATION, this.bootNotificationRequest, {
2125 skipBufferingOnError: true,
2126 });
2127 }
2128 // Start WebSocket ping
2129 this.startWebSocketPing();
2130 // Start heartbeat
2131 this.startHeartbeat();
2132 // Initialize connectors status
2133 if (this.hasEvses) {
2134 for (const [evseId, evseStatus] of this.evses) {
2135 if (evseId > 0) {
2136 for (const [connectorId, connectorStatus] of evseStatus.connectors) {
2137 const connectorBootStatus = getBootConnectorStatus(this, connectorId, connectorStatus);
2138 await OCPPServiceUtils.sendAndSetConnectorStatus(
2139 this,
2140 connectorId,
2141 connectorBootStatus,
2142 evseId,
2143 );
2144 }
2145 }
2146 }
2147 } else {
2148 for (const connectorId of this.connectors.keys()) {
2149 if (connectorId > 0) {
2150 const connectorBootStatus = getBootConnectorStatus(
2151 this,
2152 connectorId,
2153 this.getConnectorStatus(connectorId)!,
2154 );
2155 await OCPPServiceUtils.sendAndSetConnectorStatus(this, connectorId, connectorBootStatus);
2156 }
2157 }
2158 }
2159 if (this.stationInfo?.firmwareStatus === FirmwareStatus.Installing) {
2160 await this.ocppRequestService.requestHandler<
2161 FirmwareStatusNotificationRequest,
2162 FirmwareStatusNotificationResponse
2163 >(this, RequestCommand.FIRMWARE_STATUS_NOTIFICATION, {
2164 status: FirmwareStatus.Installed,
2165 });
2166 this.stationInfo.firmwareStatus = FirmwareStatus.Installed;
2167 }
2168
2169 // Start the ATG
2170 if (this.getAutomaticTransactionGeneratorConfiguration()?.enable === true) {
2171 this.startAutomaticTransactionGenerator();
2172 }
2173 this.wsConnectionRestarted === true && this.flushMessageBuffer();
2174 }
2175
2176 private async stopMessageSequence(
2177 reason: StopTransactionReason = StopTransactionReason.NONE,
2178 ): Promise<void> {
2179 // Stop WebSocket ping
2180 this.stopWebSocketPing();
2181 // Stop heartbeat
2182 this.stopHeartbeat();
2183 // Stop ongoing transactions
2184 if (this.automaticTransactionGenerator?.started === true) {
2185 await this.stopAutomaticTransactionGenerator();
2186 } else {
2187 await this.stopRunningTransactions(reason);
2188 }
2189 if (this.hasEvses) {
2190 for (const [evseId, evseStatus] of this.evses) {
2191 if (evseId > 0) {
2192 for (const [connectorId, connectorStatus] of evseStatus.connectors) {
2193 await this.ocppRequestService.requestHandler<
2194 StatusNotificationRequest,
2195 StatusNotificationResponse
2196 >(
2197 this,
2198 RequestCommand.STATUS_NOTIFICATION,
2199 OCPPServiceUtils.buildStatusNotificationRequest(
2200 this,
2201 connectorId,
2202 ConnectorStatusEnum.Unavailable,
2203 evseId,
2204 ),
2205 );
2206 delete connectorStatus?.status;
2207 }
2208 }
2209 }
2210 } else {
2211 for (const connectorId of this.connectors.keys()) {
2212 if (connectorId > 0) {
2213 await this.ocppRequestService.requestHandler<
2214 StatusNotificationRequest,
2215 StatusNotificationResponse
2216 >(
2217 this,
2218 RequestCommand.STATUS_NOTIFICATION,
2219 OCPPServiceUtils.buildStatusNotificationRequest(
2220 this,
2221 connectorId,
2222 ConnectorStatusEnum.Unavailable,
2223 ),
2224 );
2225 delete this.getConnectorStatus(connectorId)?.status;
2226 }
2227 }
2228 }
2229 }
2230
2231 private startWebSocketPing(): void {
2232 const webSocketPingInterval: number = getConfigurationKey(
2233 this,
2234 StandardParametersKey.WebSocketPingInterval,
2235 )
2236 ? convertToInt(getConfigurationKey(this, StandardParametersKey.WebSocketPingInterval)?.value)
2237 : 0;
2238 if (webSocketPingInterval > 0 && !this.webSocketPingSetInterval) {
2239 this.webSocketPingSetInterval = setInterval(() => {
2240 if (this.isWebSocketConnectionOpened() === true) {
2241 this.wsConnection?.ping();
2242 }
2243 }, secondsToMilliseconds(webSocketPingInterval));
2244 logger.info(
2245 `${this.logPrefix()} WebSocket ping started every ${formatDurationSeconds(
2246 webSocketPingInterval,
2247 )}`,
2248 );
2249 } else if (this.webSocketPingSetInterval) {
2250 logger.info(
2251 `${this.logPrefix()} WebSocket ping already started every ${formatDurationSeconds(
2252 webSocketPingInterval,
2253 )}`,
2254 );
2255 } else {
2256 logger.error(
2257 `${this.logPrefix()} WebSocket ping interval set to ${webSocketPingInterval}, not starting the WebSocket ping`,
2258 );
2259 }
2260 }
2261
2262 private stopWebSocketPing(): void {
2263 if (this.webSocketPingSetInterval) {
2264 clearInterval(this.webSocketPingSetInterval);
2265 delete this.webSocketPingSetInterval;
2266 }
2267 }
2268
2269 private getConfiguredSupervisionUrl(): URL {
2270 let configuredSupervisionUrl: string;
2271 const supervisionUrls = this.stationInfo?.supervisionUrls ?? Configuration.getSupervisionUrls();
2272 if (isNotEmptyArray(supervisionUrls)) {
2273 let configuredSupervisionUrlIndex: number;
2274 switch (Configuration.getSupervisionUrlDistribution()) {
2275 case SupervisionUrlDistribution.RANDOM:
2276 configuredSupervisionUrlIndex = Math.floor(
2277 secureRandom() * (supervisionUrls as string[]).length,
2278 );
2279 break;
2280 case SupervisionUrlDistribution.ROUND_ROBIN:
2281 case SupervisionUrlDistribution.CHARGING_STATION_AFFINITY:
2282 default:
2283 Object.values(SupervisionUrlDistribution).includes(
2284 Configuration.getSupervisionUrlDistribution()!,
2285 ) === false &&
2286 logger.error(
2287 // eslint-disable-next-line @typescript-eslint/no-base-to-string
2288 `${this.logPrefix()} Unknown supervision url distribution '${Configuration.getSupervisionUrlDistribution()}' from values '${SupervisionUrlDistribution.toString()}', defaulting to ${
2289 SupervisionUrlDistribution.CHARGING_STATION_AFFINITY
2290 }`,
2291 );
2292 configuredSupervisionUrlIndex = (this.index - 1) % (supervisionUrls as string[]).length;
2293 break;
2294 }
2295 configuredSupervisionUrl = (supervisionUrls as string[])[configuredSupervisionUrlIndex];
2296 } else {
2297 configuredSupervisionUrl = supervisionUrls as string;
2298 }
2299 if (isNotEmptyString(configuredSupervisionUrl)) {
2300 return new URL(configuredSupervisionUrl);
2301 }
2302 const errorMsg = 'No supervision url(s) configured';
2303 logger.error(`${this.logPrefix()} ${errorMsg}`);
2304 throw new BaseError(`${errorMsg}`);
2305 }
2306
2307 private stopHeartbeat(): void {
2308 if (this.heartbeatSetInterval) {
2309 clearInterval(this.heartbeatSetInterval);
2310 delete this.heartbeatSetInterval;
2311 }
2312 }
2313
2314 private terminateWSConnection(): void {
2315 if (this.isWebSocketConnectionOpened() === true) {
2316 this.wsConnection?.terminate();
2317 this.wsConnection = null;
2318 }
2319 }
2320
2321 private getReconnectExponentialDelay(): boolean {
2322 return this.stationInfo?.reconnectExponentialDelay ?? false;
2323 }
2324
2325 private async reconnect(): Promise<void> {
2326 // Stop WebSocket ping
2327 this.stopWebSocketPing();
2328 // Stop heartbeat
2329 this.stopHeartbeat();
2330 // Stop the ATG if needed
2331 if (this.getAutomaticTransactionGeneratorConfiguration().stopOnConnectionFailure === true) {
2332 await this.stopAutomaticTransactionGenerator();
2333 }
2334 if (
2335 this.autoReconnectRetryCount < this.getAutoReconnectMaxRetries()! ||
2336 this.getAutoReconnectMaxRetries() === -1
2337 ) {
2338 ++this.autoReconnectRetryCount;
2339 const reconnectDelay = this.getReconnectExponentialDelay()
2340 ? exponentialDelay(this.autoReconnectRetryCount)
2341 : secondsToMilliseconds(this.getConnectionTimeout());
2342 const reconnectDelayWithdraw = 1000;
2343 const reconnectTimeout =
2344 reconnectDelay && reconnectDelay - reconnectDelayWithdraw > 0
2345 ? reconnectDelay - reconnectDelayWithdraw
2346 : 0;
2347 logger.error(
2348 `${this.logPrefix()} WebSocket connection retry in ${roundTo(
2349 reconnectDelay,
2350 2,
2351 )}ms, timeout ${reconnectTimeout}ms`,
2352 );
2353 await sleep(reconnectDelay);
2354 logger.error(
2355 `${this.logPrefix()} WebSocket connection retry #${this.autoReconnectRetryCount.toString()}`,
2356 );
2357 this.openWSConnection(
2358 {
2359 handshakeTimeout: reconnectTimeout,
2360 },
2361 { closeOpened: true },
2362 );
2363 this.wsConnectionRestarted = true;
2364 } else if (this.getAutoReconnectMaxRetries() !== -1) {
2365 logger.error(
2366 `${this.logPrefix()} WebSocket connection retries failure: maximum retries reached (${
2367 this.autoReconnectRetryCount
2368 }) or retries disabled (${this.getAutoReconnectMaxRetries()})`,
2369 );
2370 }
2371 }
2372 }