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