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