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