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