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