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