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