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