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