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