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