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