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