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