419e89e36a8f768952e9c15976d2f28fdce09900
[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.templateFile,
317 this.logPrefix()
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 if (Utils.isNullOrUndefined(stationTemplate)) {
946 const errorMsg = `Failed to read charging station template file ${this.templateFile}`;
947 logger.error(`${this.logPrefix()} ${errorMsg}`);
948 throw new BaseError(errorMsg);
949 }
950 if (Utils.isEmptyObject(stationTemplate)) {
951 const errorMsg = `Empty charging station information from template file ${this.templateFile}`;
952 logger.error(`${this.logPrefix()} ${errorMsg}`);
953 throw new BaseError(errorMsg);
954 }
955 ChargingStationUtils.warnTemplateKeysDeprecation(
956 this.templateFile,
957 stationTemplate,
958 this.logPrefix()
959 );
960 const stationInfo: ChargingStationInfo =
961 ChargingStationUtils.stationTemplateToStationInfo(stationTemplate);
962 stationInfo.hashId = ChargingStationUtils.getHashId(this.index, stationTemplate);
963 stationInfo.chargingStationId = ChargingStationUtils.getChargingStationId(
964 this.index,
965 stationTemplate
966 );
967 stationInfo.ocppVersion = stationTemplate?.ocppVersion ?? OCPPVersion.VERSION_16;
968 ChargingStationUtils.createSerialNumber(stationTemplate, stationInfo);
969 if (Utils.isNotEmptyArray(stationTemplate?.power)) {
970 stationTemplate.power = stationTemplate.power as number[];
971 const powerArrayRandomIndex = Math.floor(Utils.secureRandom() * stationTemplate.power.length);
972 stationInfo.maximumPower =
973 stationTemplate?.powerUnit === PowerUnits.KILO_WATT
974 ? stationTemplate.power[powerArrayRandomIndex] * 1000
975 : stationTemplate.power[powerArrayRandomIndex];
976 } else {
977 stationTemplate.power = stationTemplate?.power as number;
978 stationInfo.maximumPower =
979 stationTemplate?.powerUnit === PowerUnits.KILO_WATT
980 ? stationTemplate.power * 1000
981 : stationTemplate.power;
982 }
983 stationInfo.firmwareVersionPattern =
984 stationTemplate?.firmwareVersionPattern ?? Constants.SEMVER_PATTERN;
985 if (
986 Utils.isNotEmptyString(stationInfo.firmwareVersion) &&
987 new RegExp(stationInfo.firmwareVersionPattern).test(stationInfo.firmwareVersion) === false
988 ) {
989 logger.warn(
990 `${this.logPrefix()} Firmware version '${stationInfo.firmwareVersion}' in template file ${
991 this.templateFile
992 } does not match firmware version pattern '${stationInfo.firmwareVersionPattern}'`
993 );
994 }
995 stationInfo.firmwareUpgrade = merge<FirmwareUpgrade>(
996 {
997 versionUpgrade: {
998 step: 1,
999 },
1000 reset: true,
1001 },
1002 stationTemplate?.firmwareUpgrade ?? {}
1003 );
1004 stationInfo.resetTime = !Utils.isNullOrUndefined(stationTemplate?.resetTime)
1005 ? stationTemplate.resetTime * 1000
1006 : Constants.CHARGING_STATION_DEFAULT_RESET_TIME;
1007 stationInfo.maximumAmperage = this.getMaximumAmperage(stationInfo);
1008 if (stationInfo?.Connectors) {
1009 ChargingStationUtils.checkConnectorsConfiguration(
1010 stationInfo,
1011 this.templateFile,
1012 this.logPrefix()
1013 );
1014 }
1015 delete stationInfo?.Connectors;
1016 delete stationInfo?.Evses;
1017 return stationInfo;
1018 }
1019
1020 private getStationInfoFromFile(): ChargingStationInfo | undefined {
1021 let stationInfo: ChargingStationInfo | undefined;
1022 if (this.getStationInfoPersistentConfiguration()) {
1023 stationInfo = this.getConfigurationFromFile()?.stationInfo;
1024 if (stationInfo) {
1025 delete stationInfo?.infoHash;
1026 }
1027 }
1028 return stationInfo;
1029 }
1030
1031 private getStationInfo(): ChargingStationInfo {
1032 const stationInfoFromTemplate: ChargingStationInfo = this.getStationInfoFromTemplate();
1033 const stationInfoFromFile: ChargingStationInfo | undefined = this.getStationInfoFromFile();
1034 // Priority:
1035 // 1. charging station info from template
1036 // 2. charging station info from configuration file
1037 if (stationInfoFromFile?.templateHash === stationInfoFromTemplate.templateHash) {
1038 return stationInfoFromFile;
1039 }
1040 stationInfoFromFile &&
1041 ChargingStationUtils.propagateSerialNumber(
1042 this.getTemplateFromFile(),
1043 stationInfoFromFile,
1044 stationInfoFromTemplate
1045 );
1046 return stationInfoFromTemplate;
1047 }
1048
1049 private saveStationInfo(): void {
1050 if (this.getStationInfoPersistentConfiguration()) {
1051 this.saveConfiguration();
1052 }
1053 }
1054
1055 private getOcppPersistentConfiguration(): boolean {
1056 return this.stationInfo?.ocppPersistentConfiguration ?? true;
1057 }
1058
1059 private getStationInfoPersistentConfiguration(): boolean {
1060 return this.stationInfo?.stationInfoPersistentConfiguration ?? true;
1061 }
1062
1063 private handleUnsupportedVersion(version: OCPPVersion) {
1064 const errorMsg = `Unsupported protocol version '${version}' configured in template file ${this.templateFile}`;
1065 logger.error(`${this.logPrefix()} ${errorMsg}`);
1066 throw new BaseError(errorMsg);
1067 }
1068
1069 private initialize(): void {
1070 const stationTemplate = this.getTemplateFromFile();
1071 if (Utils.isNullOrUndefined(stationTemplate)) {
1072 const errorMsg = `Failed to read charging station template file ${this.templateFile}`;
1073 logger.error(`${this.logPrefix()} ${errorMsg}`);
1074 throw new BaseError(errorMsg);
1075 }
1076 this.configurationFile = path.join(
1077 path.dirname(this.templateFile.replace('station-templates', 'configurations')),
1078 `${ChargingStationUtils.getHashId(this.index, stationTemplate)}.json`
1079 );
1080 this.initializeConnectorsOrEvses(stationTemplate);
1081 this.stationInfo = this.getStationInfo();
1082 if (
1083 this.stationInfo.firmwareStatus === FirmwareStatus.Installing &&
1084 Utils.isNotEmptyString(this.stationInfo.firmwareVersion) &&
1085 Utils.isNotEmptyString(this.stationInfo.firmwareVersionPattern)
1086 ) {
1087 const patternGroup: number | undefined =
1088 this.stationInfo.firmwareUpgrade?.versionUpgrade?.patternGroup ??
1089 this.stationInfo.firmwareVersion?.split('.').length;
1090 const match = this.stationInfo?.firmwareVersion
1091 ?.match(new RegExp(this.stationInfo.firmwareVersionPattern))
1092 ?.slice(1, patternGroup + 1);
1093 const patchLevelIndex = match.length - 1;
1094 match[patchLevelIndex] = (
1095 Utils.convertToInt(match[patchLevelIndex]) +
1096 this.stationInfo.firmwareUpgrade?.versionUpgrade?.step
1097 ).toString();
1098 this.stationInfo.firmwareVersion = match?.join('.');
1099 }
1100 this.saveStationInfo();
1101 this.configuredSupervisionUrl = this.getConfiguredSupervisionUrl();
1102 if (this.getEnableStatistics() === true) {
1103 this.performanceStatistics = PerformanceStatistics.getInstance(
1104 this.stationInfo.hashId,
1105 this.stationInfo.chargingStationId,
1106 this.configuredSupervisionUrl
1107 );
1108 }
1109 this.bootNotificationRequest = ChargingStationUtils.createBootNotificationRequest(
1110 this.stationInfo
1111 );
1112 this.powerDivider = this.getPowerDivider();
1113 // OCPP configuration
1114 this.ocppConfiguration = this.getOcppConfiguration();
1115 this.initializeOcppConfiguration();
1116 this.initializeOcppServices();
1117 if (this.stationInfo?.autoRegister === true) {
1118 this.bootNotificationResponse = {
1119 currentTime: new Date(),
1120 interval: this.getHeartbeatInterval() / 1000,
1121 status: RegistrationStatusEnumType.ACCEPTED,
1122 };
1123 }
1124 }
1125
1126 private initializeOcppServices(): void {
1127 const ocppVersion = this.stationInfo.ocppVersion ?? OCPPVersion.VERSION_16;
1128 switch (ocppVersion) {
1129 case OCPPVersion.VERSION_16:
1130 this.ocppIncomingRequestService =
1131 OCPP16IncomingRequestService.getInstance<OCPP16IncomingRequestService>();
1132 this.ocppRequestService = OCPP16RequestService.getInstance<OCPP16RequestService>(
1133 OCPP16ResponseService.getInstance<OCPP16ResponseService>()
1134 );
1135 break;
1136 case OCPPVersion.VERSION_20:
1137 case OCPPVersion.VERSION_201:
1138 this.ocppIncomingRequestService =
1139 OCPP20IncomingRequestService.getInstance<OCPP20IncomingRequestService>();
1140 this.ocppRequestService = OCPP20RequestService.getInstance<OCPP20RequestService>(
1141 OCPP20ResponseService.getInstance<OCPP20ResponseService>()
1142 );
1143 break;
1144 default:
1145 this.handleUnsupportedVersion(ocppVersion);
1146 break;
1147 }
1148 }
1149
1150 private initializeOcppConfiguration(): void {
1151 if (
1152 !ChargingStationConfigurationUtils.getConfigurationKey(
1153 this,
1154 StandardParametersKey.HeartbeatInterval
1155 )
1156 ) {
1157 ChargingStationConfigurationUtils.addConfigurationKey(
1158 this,
1159 StandardParametersKey.HeartbeatInterval,
1160 '0'
1161 );
1162 }
1163 if (
1164 !ChargingStationConfigurationUtils.getConfigurationKey(
1165 this,
1166 StandardParametersKey.HeartBeatInterval
1167 )
1168 ) {
1169 ChargingStationConfigurationUtils.addConfigurationKey(
1170 this,
1171 StandardParametersKey.HeartBeatInterval,
1172 '0',
1173 { visible: false }
1174 );
1175 }
1176 if (
1177 this.getSupervisionUrlOcppConfiguration() &&
1178 Utils.isNotEmptyString(this.getSupervisionUrlOcppKey()) &&
1179 !ChargingStationConfigurationUtils.getConfigurationKey(this, this.getSupervisionUrlOcppKey())
1180 ) {
1181 ChargingStationConfigurationUtils.addConfigurationKey(
1182 this,
1183 this.getSupervisionUrlOcppKey(),
1184 this.configuredSupervisionUrl.href,
1185 { reboot: true }
1186 );
1187 } else if (
1188 !this.getSupervisionUrlOcppConfiguration() &&
1189 Utils.isNotEmptyString(this.getSupervisionUrlOcppKey()) &&
1190 ChargingStationConfigurationUtils.getConfigurationKey(this, this.getSupervisionUrlOcppKey())
1191 ) {
1192 ChargingStationConfigurationUtils.deleteConfigurationKey(
1193 this,
1194 this.getSupervisionUrlOcppKey(),
1195 { save: false }
1196 );
1197 }
1198 if (
1199 Utils.isNotEmptyString(this.stationInfo?.amperageLimitationOcppKey) &&
1200 !ChargingStationConfigurationUtils.getConfigurationKey(
1201 this,
1202 this.stationInfo.amperageLimitationOcppKey
1203 )
1204 ) {
1205 ChargingStationConfigurationUtils.addConfigurationKey(
1206 this,
1207 this.stationInfo.amperageLimitationOcppKey,
1208 (
1209 this.stationInfo.maximumAmperage *
1210 ChargingStationUtils.getAmperageLimitationUnitDivider(this.stationInfo)
1211 ).toString()
1212 );
1213 }
1214 if (
1215 !ChargingStationConfigurationUtils.getConfigurationKey(
1216 this,
1217 StandardParametersKey.SupportedFeatureProfiles
1218 )
1219 ) {
1220 ChargingStationConfigurationUtils.addConfigurationKey(
1221 this,
1222 StandardParametersKey.SupportedFeatureProfiles,
1223 `${SupportedFeatureProfiles.Core},${SupportedFeatureProfiles.FirmwareManagement},${SupportedFeatureProfiles.LocalAuthListManagement},${SupportedFeatureProfiles.SmartCharging},${SupportedFeatureProfiles.RemoteTrigger}`
1224 );
1225 }
1226 ChargingStationConfigurationUtils.addConfigurationKey(
1227 this,
1228 StandardParametersKey.NumberOfConnectors,
1229 this.getNumberOfConnectors().toString(),
1230 { readonly: true },
1231 { overwrite: true }
1232 );
1233 if (
1234 !ChargingStationConfigurationUtils.getConfigurationKey(
1235 this,
1236 StandardParametersKey.MeterValuesSampledData
1237 )
1238 ) {
1239 ChargingStationConfigurationUtils.addConfigurationKey(
1240 this,
1241 StandardParametersKey.MeterValuesSampledData,
1242 MeterValueMeasurand.ENERGY_ACTIVE_IMPORT_REGISTER
1243 );
1244 }
1245 if (
1246 !ChargingStationConfigurationUtils.getConfigurationKey(
1247 this,
1248 StandardParametersKey.ConnectorPhaseRotation
1249 )
1250 ) {
1251 const connectorsPhaseRotation: string[] = [];
1252 if (this.hasEvses) {
1253 for (const evseStatus of this.evses.values()) {
1254 for (const connectorId of evseStatus.connectors.keys()) {
1255 connectorsPhaseRotation.push(
1256 ChargingStationUtils.getPhaseRotationValue(connectorId, this.getNumberOfPhases())
1257 );
1258 }
1259 }
1260 } else {
1261 for (const connectorId of this.connectors.keys()) {
1262 connectorsPhaseRotation.push(
1263 ChargingStationUtils.getPhaseRotationValue(connectorId, this.getNumberOfPhases())
1264 );
1265 }
1266 }
1267 ChargingStationConfigurationUtils.addConfigurationKey(
1268 this,
1269 StandardParametersKey.ConnectorPhaseRotation,
1270 connectorsPhaseRotation.toString()
1271 );
1272 }
1273 if (
1274 !ChargingStationConfigurationUtils.getConfigurationKey(
1275 this,
1276 StandardParametersKey.AuthorizeRemoteTxRequests
1277 )
1278 ) {
1279 ChargingStationConfigurationUtils.addConfigurationKey(
1280 this,
1281 StandardParametersKey.AuthorizeRemoteTxRequests,
1282 'true'
1283 );
1284 }
1285 if (
1286 !ChargingStationConfigurationUtils.getConfigurationKey(
1287 this,
1288 StandardParametersKey.LocalAuthListEnabled
1289 ) &&
1290 ChargingStationConfigurationUtils.getConfigurationKey(
1291 this,
1292 StandardParametersKey.SupportedFeatureProfiles
1293 )?.value?.includes(SupportedFeatureProfiles.LocalAuthListManagement)
1294 ) {
1295 ChargingStationConfigurationUtils.addConfigurationKey(
1296 this,
1297 StandardParametersKey.LocalAuthListEnabled,
1298 'false'
1299 );
1300 }
1301 if (
1302 !ChargingStationConfigurationUtils.getConfigurationKey(
1303 this,
1304 StandardParametersKey.ConnectionTimeOut
1305 )
1306 ) {
1307 ChargingStationConfigurationUtils.addConfigurationKey(
1308 this,
1309 StandardParametersKey.ConnectionTimeOut,
1310 Constants.DEFAULT_CONNECTION_TIMEOUT.toString()
1311 );
1312 }
1313 this.saveOcppConfiguration();
1314 }
1315
1316 private initializeConnectorsOrEvses(stationTemplate: ChargingStationTemplate) {
1317 if (stationTemplate?.Connectors && !stationTemplate?.Evses) {
1318 this.initializeConnectors(stationTemplate);
1319 } else if (stationTemplate?.Evses && !stationTemplate?.Connectors) {
1320 this.initializeEvses(stationTemplate);
1321 } else if (stationTemplate?.Evses && stationTemplate?.Connectors) {
1322 const errorMsg = `Connectors and evses defined at the same time in template file ${this.templateFile}`;
1323 logger.error(`${this.logPrefix()} ${errorMsg}`);
1324 throw new BaseError(errorMsg);
1325 } else {
1326 const errorMsg = `No connectors or evses defined in template file ${this.templateFile}`;
1327 logger.error(`${this.logPrefix()} ${errorMsg}`);
1328 throw new BaseError(errorMsg);
1329 }
1330 }
1331
1332 private initializeConnectors(stationTemplate: ChargingStationTemplate): void {
1333 if (!stationTemplate?.Connectors && this.connectors.size === 0) {
1334 const errorMsg = `No already defined connectors and charging station information from template ${this.templateFile} with no connectors configuration defined`;
1335 logger.error(`${this.logPrefix()} ${errorMsg}`);
1336 throw new BaseError(errorMsg);
1337 }
1338 if (!stationTemplate?.Connectors[0]) {
1339 logger.warn(
1340 `${this.logPrefix()} Charging station information from template ${
1341 this.templateFile
1342 } with no connector id 0 configuration`
1343 );
1344 }
1345 if (stationTemplate?.Connectors) {
1346 const { configuredMaxConnectors, templateMaxConnectors, templateMaxAvailableConnectors } =
1347 ChargingStationUtils.checkConnectorsConfiguration(
1348 stationTemplate,
1349 this.templateFile,
1350 this.logPrefix()
1351 );
1352 const connectorsConfigHash = crypto
1353 .createHash(Constants.DEFAULT_HASH_ALGORITHM)
1354 .update(
1355 `${JSON.stringify(stationTemplate?.Connectors)}${configuredMaxConnectors.toString()}`
1356 )
1357 .digest('hex');
1358 const connectorsConfigChanged =
1359 this.connectors?.size !== 0 && this.connectorsConfigurationHash !== connectorsConfigHash;
1360 if (this.connectors?.size === 0 || connectorsConfigChanged) {
1361 connectorsConfigChanged && this.connectors.clear();
1362 this.connectorsConfigurationHash = connectorsConfigHash;
1363 if (templateMaxConnectors > 0) {
1364 for (let connectorId = 0; connectorId <= configuredMaxConnectors; connectorId++) {
1365 if (
1366 connectorId === 0 &&
1367 (!stationTemplate?.Connectors[connectorId] ||
1368 this.getUseConnectorId0(stationTemplate) === false)
1369 ) {
1370 continue;
1371 }
1372 const templateConnectorId =
1373 connectorId > 0 && stationTemplate?.randomConnectors
1374 ? Utils.getRandomInteger(templateMaxAvailableConnectors, 1)
1375 : connectorId;
1376 const connectorStatus = stationTemplate?.Connectors[templateConnectorId];
1377 ChargingStationUtils.checkStationInfoConnectorStatus(
1378 templateConnectorId,
1379 connectorStatus,
1380 this.logPrefix(),
1381 this.templateFile
1382 );
1383 this.connectors.set(connectorId, Utils.cloneObject<ConnectorStatus>(connectorStatus));
1384 }
1385 ChargingStationUtils.initializeConnectorsMapStatus(this.connectors, this.logPrefix());
1386 this.saveConnectorsStatus();
1387 } else {
1388 logger.warn(
1389 `${this.logPrefix()} Charging station information from template ${
1390 this.templateFile
1391 } with no connectors configuration defined, cannot create connectors`
1392 );
1393 }
1394 }
1395 } else {
1396 logger.warn(
1397 `${this.logPrefix()} Charging station information from template ${
1398 this.templateFile
1399 } with no connectors configuration defined, using already defined connectors`
1400 );
1401 }
1402 }
1403
1404 private initializeEvses(stationTemplate: ChargingStationTemplate): void {
1405 if (!stationTemplate?.Evses && this.evses.size === 0) {
1406 const errorMsg = `No already defined evses and charging station information from template ${this.templateFile} with no evses configuration defined`;
1407 logger.error(`${this.logPrefix()} ${errorMsg}`);
1408 throw new BaseError(errorMsg);
1409 }
1410 if (!stationTemplate?.Evses[0]) {
1411 logger.warn(
1412 `${this.logPrefix()} Charging station information from template ${
1413 this.templateFile
1414 } with no evse id 0 configuration`
1415 );
1416 }
1417 if (!stationTemplate?.Evses[0]?.Connectors[0]) {
1418 logger.warn(
1419 `${this.logPrefix()} Charging station information from template ${
1420 this.templateFile
1421 } with evse id 0 with no connector id 0 configuration`
1422 );
1423 }
1424 if (stationTemplate?.Evses) {
1425 const evsesConfigHash = crypto
1426 .createHash(Constants.DEFAULT_HASH_ALGORITHM)
1427 .update(`${JSON.stringify(stationTemplate?.Evses)}`)
1428 .digest('hex');
1429 const evsesConfigChanged =
1430 this.evses?.size !== 0 && this.evsesConfigurationHash !== evsesConfigHash;
1431 if (this.evses?.size === 0 || evsesConfigChanged) {
1432 evsesConfigChanged && this.evses.clear();
1433 this.evsesConfigurationHash = evsesConfigHash;
1434 const templateMaxEvses = ChargingStationUtils.getMaxNumberOfEvses(stationTemplate?.Evses);
1435 if (templateMaxEvses > 0) {
1436 for (const evse in stationTemplate.Evses) {
1437 const evseId = Utils.convertToInt(evse);
1438 this.evses.set(evseId, {
1439 connectors: ChargingStationUtils.buildConnectorsMap(
1440 stationTemplate?.Evses[evse]?.Connectors,
1441 this.logPrefix(),
1442 this.templateFile
1443 ),
1444 availability: AvailabilityType.Operative,
1445 });
1446 ChargingStationUtils.initializeConnectorsMapStatus(
1447 this.evses.get(evseId)?.connectors,
1448 this.logPrefix()
1449 );
1450 }
1451 this.saveEvsesStatus();
1452 } else {
1453 logger.warn(
1454 `${this.logPrefix()} Charging station information from template ${
1455 this.templateFile
1456 } with no evses configuration defined, cannot create evses`
1457 );
1458 }
1459 }
1460 } else {
1461 logger.warn(
1462 `${this.logPrefix()} Charging station information from template ${
1463 this.templateFile
1464 } with no evses configuration defined, using already defined evses`
1465 );
1466 }
1467 }
1468
1469 private getConfigurationFromFile(): ChargingStationConfiguration | undefined {
1470 let configuration: ChargingStationConfiguration | undefined;
1471 if (this.configurationFile && fs.existsSync(this.configurationFile)) {
1472 try {
1473 if (this.sharedLRUCache.hasChargingStationConfiguration(this.configurationFileHash)) {
1474 configuration = this.sharedLRUCache.getChargingStationConfiguration(
1475 this.configurationFileHash
1476 );
1477 } else {
1478 const measureId = `${FileType.ChargingStationConfiguration} read`;
1479 const beginId = PerformanceStatistics.beginMeasure(measureId);
1480 configuration = JSON.parse(
1481 fs.readFileSync(this.configurationFile, 'utf8')
1482 ) as ChargingStationConfiguration;
1483 PerformanceStatistics.endMeasure(measureId, beginId);
1484 this.sharedLRUCache.setChargingStationConfiguration(configuration);
1485 this.configurationFileHash = configuration.configurationHash;
1486 }
1487 } catch (error) {
1488 FileUtils.handleFileException(
1489 this.configurationFile,
1490 FileType.ChargingStationConfiguration,
1491 error as NodeJS.ErrnoException,
1492 this.logPrefix()
1493 );
1494 }
1495 }
1496 return configuration;
1497 }
1498
1499 private saveConnectorsStatus() {
1500 if (this.getOcppPersistentConfiguration()) {
1501 this.saveConfiguration();
1502 }
1503 }
1504
1505 private saveEvsesStatus() {
1506 if (this.getOcppPersistentConfiguration()) {
1507 this.saveConfiguration();
1508 }
1509 }
1510
1511 private saveConfiguration(): void {
1512 if (this.configurationFile) {
1513 try {
1514 if (!fs.existsSync(path.dirname(this.configurationFile))) {
1515 fs.mkdirSync(path.dirname(this.configurationFile), { recursive: true });
1516 }
1517 const configurationData: ChargingStationConfiguration =
1518 Utils.cloneObject(this.getConfigurationFromFile()) ?? {};
1519 if (this.stationInfo) {
1520 configurationData.stationInfo = this.stationInfo;
1521 }
1522 if (this.ocppConfiguration?.configurationKey) {
1523 configurationData.configurationKey = this.ocppConfiguration.configurationKey;
1524 }
1525 if (this.connectors.size > 0) {
1526 configurationData.connectorsStatus = [...this.connectors.values()].map(
1527 // eslint-disable-next-line @typescript-eslint/no-unused-vars
1528 ({ transactionSetInterval, ...connectorStatusRest }) => connectorStatusRest
1529 );
1530 }
1531 if (this.evses.size > 0) {
1532 configurationData.evsesStatus = [...this.evses.values()].map((evseStatus) => {
1533 const status = {
1534 ...evseStatus,
1535 connectorsStatus: [...evseStatus.connectors.values()].map(
1536 // eslint-disable-next-line @typescript-eslint/no-unused-vars
1537 ({ transactionSetInterval, ...connectorStatusRest }) => connectorStatusRest
1538 ),
1539 };
1540 delete status.connectors;
1541 return status as EvseStatusConfiguration;
1542 });
1543 }
1544 delete configurationData.configurationHash;
1545 const configurationHash = crypto
1546 .createHash(Constants.DEFAULT_HASH_ALGORITHM)
1547 .update(JSON.stringify(configurationData))
1548 .digest('hex');
1549 if (this.configurationFileHash !== configurationHash) {
1550 configurationData.configurationHash = configurationHash;
1551 const measureId = `${FileType.ChargingStationConfiguration} write`;
1552 const beginId = PerformanceStatistics.beginMeasure(measureId);
1553 const fileDescriptor = fs.openSync(this.configurationFile, 'w');
1554 fs.writeFileSync(fileDescriptor, JSON.stringify(configurationData, null, 2), 'utf8');
1555 fs.closeSync(fileDescriptor);
1556 PerformanceStatistics.endMeasure(measureId, beginId);
1557 this.sharedLRUCache.deleteChargingStationConfiguration(this.configurationFileHash);
1558 this.sharedLRUCache.setChargingStationConfiguration(configurationData);
1559 this.configurationFileHash = configurationHash;
1560 } else {
1561 logger.debug(
1562 `${this.logPrefix()} Not saving unchanged charging station configuration file ${
1563 this.configurationFile
1564 }`
1565 );
1566 }
1567 } catch (error) {
1568 FileUtils.handleFileException(
1569 this.configurationFile,
1570 FileType.ChargingStationConfiguration,
1571 error as NodeJS.ErrnoException,
1572 this.logPrefix()
1573 );
1574 }
1575 } else {
1576 logger.error(
1577 `${this.logPrefix()} Trying to save charging station configuration to undefined configuration file`
1578 );
1579 }
1580 }
1581
1582 private getOcppConfigurationFromTemplate(): ChargingStationOcppConfiguration | undefined {
1583 return this.getTemplateFromFile()?.Configuration;
1584 }
1585
1586 private getOcppConfigurationFromFile(): ChargingStationOcppConfiguration | undefined {
1587 let configuration: ChargingStationConfiguration | undefined;
1588 if (this.getOcppPersistentConfiguration() === true) {
1589 const configurationFromFile = this.getConfigurationFromFile();
1590 configuration = configurationFromFile?.configurationKey && configurationFromFile;
1591 }
1592 if (!Utils.isNullOrUndefined(configuration)) {
1593 delete configuration.stationInfo;
1594 delete configuration.configurationHash;
1595 }
1596 return configuration;
1597 }
1598
1599 private getOcppConfiguration(): ChargingStationOcppConfiguration | undefined {
1600 let ocppConfiguration: ChargingStationOcppConfiguration | undefined =
1601 this.getOcppConfigurationFromFile();
1602 if (!ocppConfiguration) {
1603 ocppConfiguration = this.getOcppConfigurationFromTemplate();
1604 }
1605 return ocppConfiguration;
1606 }
1607
1608 private async onOpen(): Promise<void> {
1609 if (this.isWebSocketConnectionOpened() === true) {
1610 logger.info(
1611 `${this.logPrefix()} Connection to OCPP server through ${this.wsConnectionUrl.toString()} succeeded`
1612 );
1613 if (this.isRegistered() === false) {
1614 // Send BootNotification
1615 let registrationRetryCount = 0;
1616 do {
1617 this.bootNotificationResponse = await this.ocppRequestService.requestHandler<
1618 BootNotificationRequest,
1619 BootNotificationResponse
1620 >(this, RequestCommand.BOOT_NOTIFICATION, this.bootNotificationRequest, {
1621 skipBufferingOnError: true,
1622 });
1623 if (this.isRegistered() === false) {
1624 this.getRegistrationMaxRetries() !== -1 && registrationRetryCount++;
1625 await Utils.sleep(
1626 this?.bootNotificationResponse?.interval
1627 ? this.bootNotificationResponse.interval * 1000
1628 : Constants.OCPP_DEFAULT_BOOT_NOTIFICATION_INTERVAL
1629 );
1630 }
1631 } while (
1632 this.isRegistered() === false &&
1633 (registrationRetryCount <= this.getRegistrationMaxRetries() ||
1634 this.getRegistrationMaxRetries() === -1)
1635 );
1636 }
1637 if (this.isRegistered() === true) {
1638 if (this.isInAcceptedState() === true) {
1639 await this.startMessageSequence();
1640 }
1641 } else {
1642 logger.error(
1643 `${this.logPrefix()} Registration failure: max retries reached (${this.getRegistrationMaxRetries()}) or retry disabled (${this.getRegistrationMaxRetries()})`
1644 );
1645 }
1646 this.wsConnectionRestarted = false;
1647 this.autoReconnectRetryCount = 0;
1648 parentPort?.postMessage(MessageChannelUtils.buildUpdatedMessage(this));
1649 } else {
1650 logger.warn(
1651 `${this.logPrefix()} Connection to OCPP server through ${this.wsConnectionUrl.toString()} failed`
1652 );
1653 }
1654 }
1655
1656 private async onClose(code: number, reason: Buffer): Promise<void> {
1657 switch (code) {
1658 // Normal close
1659 case WebSocketCloseEventStatusCode.CLOSE_NORMAL:
1660 case WebSocketCloseEventStatusCode.CLOSE_NO_STATUS:
1661 logger.info(
1662 `${this.logPrefix()} WebSocket normally closed with status '${Utils.getWebSocketCloseEventStatusString(
1663 code
1664 )}' and reason '${reason.toString()}'`
1665 );
1666 this.autoReconnectRetryCount = 0;
1667 break;
1668 // Abnormal close
1669 default:
1670 logger.error(
1671 `${this.logPrefix()} WebSocket abnormally closed with status '${Utils.getWebSocketCloseEventStatusString(
1672 code
1673 )}' and reason '${reason.toString()}'`
1674 );
1675 this.started === true && (await this.reconnect());
1676 break;
1677 }
1678 parentPort?.postMessage(MessageChannelUtils.buildUpdatedMessage(this));
1679 }
1680
1681 private getCachedRequest(messageType: MessageType, messageId: string): CachedRequest | undefined {
1682 const cachedRequest = this.requests.get(messageId);
1683 if (Array.isArray(cachedRequest) === true) {
1684 return cachedRequest;
1685 }
1686 throw new OCPPError(
1687 ErrorType.PROTOCOL_ERROR,
1688 `Cached request for message id ${messageId} ${OCPPServiceUtils.getMessageTypeString(
1689 messageType
1690 )} is not an array`,
1691 undefined,
1692 cachedRequest as JsonType
1693 );
1694 }
1695
1696 private async handleIncomingMessage(request: IncomingRequest): Promise<void> {
1697 const [messageType, messageId, commandName, commandPayload] = request;
1698 if (this.getEnableStatistics() === true) {
1699 this.performanceStatistics?.addRequestStatistic(commandName, messageType);
1700 }
1701 logger.debug(
1702 `${this.logPrefix()} << Command '${commandName}' received request payload: ${JSON.stringify(
1703 request
1704 )}`
1705 );
1706 // Process the message
1707 await this.ocppIncomingRequestService.incomingRequestHandler(
1708 this,
1709 messageId,
1710 commandName,
1711 commandPayload
1712 );
1713 }
1714
1715 private handleResponseMessage(response: Response): void {
1716 const [messageType, messageId, commandPayload] = response;
1717 if (this.requests.has(messageId) === false) {
1718 // Error
1719 throw new OCPPError(
1720 ErrorType.INTERNAL_ERROR,
1721 `Response for unknown message id ${messageId}`,
1722 undefined,
1723 commandPayload
1724 );
1725 }
1726 // Respond
1727 const [responseCallback, , requestCommandName, requestPayload] = this.getCachedRequest(
1728 messageType,
1729 messageId
1730 );
1731 logger.debug(
1732 `${this.logPrefix()} << Command '${
1733 requestCommandName ?? Constants.UNKNOWN_COMMAND
1734 }' received response payload: ${JSON.stringify(response)}`
1735 );
1736 responseCallback(commandPayload, requestPayload);
1737 }
1738
1739 private handleErrorMessage(errorResponse: ErrorResponse): void {
1740 const [messageType, messageId, errorType, errorMessage, errorDetails] = errorResponse;
1741 if (this.requests.has(messageId) === false) {
1742 // Error
1743 throw new OCPPError(
1744 ErrorType.INTERNAL_ERROR,
1745 `Error response for unknown message id ${messageId}`,
1746 undefined,
1747 { errorType, errorMessage, errorDetails }
1748 );
1749 }
1750 const [, errorCallback, requestCommandName] = this.getCachedRequest(messageType, messageId);
1751 logger.debug(
1752 `${this.logPrefix()} << Command '${
1753 requestCommandName ?? Constants.UNKNOWN_COMMAND
1754 }' received error response payload: ${JSON.stringify(errorResponse)}`
1755 );
1756 errorCallback(new OCPPError(errorType, errorMessage, requestCommandName, errorDetails));
1757 }
1758
1759 private async onMessage(data: RawData): Promise<void> {
1760 let request: IncomingRequest | Response | ErrorResponse;
1761 let messageType: number;
1762 let errorMsg: string;
1763 try {
1764 request = JSON.parse(data.toString()) as IncomingRequest | Response | ErrorResponse;
1765 if (Array.isArray(request) === true) {
1766 [messageType] = request;
1767 // Check the type of message
1768 switch (messageType) {
1769 // Incoming Message
1770 case MessageType.CALL_MESSAGE:
1771 await this.handleIncomingMessage(request as IncomingRequest);
1772 break;
1773 // Response Message
1774 case MessageType.CALL_RESULT_MESSAGE:
1775 this.handleResponseMessage(request as Response);
1776 break;
1777 // Error Message
1778 case MessageType.CALL_ERROR_MESSAGE:
1779 this.handleErrorMessage(request as ErrorResponse);
1780 break;
1781 // Unknown Message
1782 default:
1783 // eslint-disable-next-line @typescript-eslint/restrict-template-expressions
1784 errorMsg = `Wrong message type ${messageType}`;
1785 logger.error(`${this.logPrefix()} ${errorMsg}`);
1786 throw new OCPPError(ErrorType.PROTOCOL_ERROR, errorMsg);
1787 }
1788 parentPort?.postMessage(MessageChannelUtils.buildUpdatedMessage(this));
1789 } else {
1790 throw new OCPPError(ErrorType.PROTOCOL_ERROR, 'Incoming message is not an array', null, {
1791 request,
1792 });
1793 }
1794 } catch (error) {
1795 let commandName: IncomingRequestCommand;
1796 let requestCommandName: RequestCommand | IncomingRequestCommand;
1797 let errorCallback: ErrorCallback;
1798 const [, messageId] = request;
1799 switch (messageType) {
1800 case MessageType.CALL_MESSAGE:
1801 [, , commandName] = request as IncomingRequest;
1802 // Send error
1803 await this.ocppRequestService.sendError(this, messageId, error as OCPPError, commandName);
1804 break;
1805 case MessageType.CALL_RESULT_MESSAGE:
1806 case MessageType.CALL_ERROR_MESSAGE:
1807 if (this.requests.has(messageId) === true) {
1808 [, errorCallback, requestCommandName] = this.getCachedRequest(messageType, messageId);
1809 // Reject the deferred promise in case of error at response handling (rejecting an already fulfilled promise is a no-op)
1810 errorCallback(error as OCPPError, false);
1811 } else {
1812 // Remove the request from the cache in case of error at response handling
1813 this.requests.delete(messageId);
1814 }
1815 break;
1816 }
1817 if (error instanceof OCPPError === false) {
1818 logger.warn(
1819 `${this.logPrefix()} Error thrown at incoming OCPP command '${
1820 commandName ?? requestCommandName ?? Constants.UNKNOWN_COMMAND
1821 }' message '${data.toString()}' handling is not an OCPPError:`,
1822 error
1823 );
1824 }
1825 logger.error(
1826 `${this.logPrefix()} Incoming OCPP command '${
1827 commandName ?? requestCommandName ?? Constants.UNKNOWN_COMMAND
1828 }' message '${data.toString()}'${
1829 messageType !== MessageType.CALL_MESSAGE
1830 ? ` matching cached request '${JSON.stringify(this.requests.get(messageId))}'`
1831 : ''
1832 } processing error:`,
1833 error
1834 );
1835 }
1836 }
1837
1838 private onPing(): void {
1839 logger.debug(`${this.logPrefix()} Received a WS ping (rfc6455) from the server`);
1840 }
1841
1842 private onPong(): void {
1843 logger.debug(`${this.logPrefix()} Received a WS pong (rfc6455) from the server`);
1844 }
1845
1846 private onError(error: WSError): void {
1847 this.closeWSConnection();
1848 logger.error(`${this.logPrefix()} WebSocket error:`, error);
1849 }
1850
1851 private getEnergyActiveImportRegister(connectorStatus: ConnectorStatus, rounded = false): number {
1852 if (this.getMeteringPerTransaction() === true) {
1853 return (
1854 (rounded === true
1855 ? Math.round(connectorStatus?.transactionEnergyActiveImportRegisterValue)
1856 : connectorStatus?.transactionEnergyActiveImportRegisterValue) ?? 0
1857 );
1858 }
1859 return (
1860 (rounded === true
1861 ? Math.round(connectorStatus?.energyActiveImportRegisterValue)
1862 : connectorStatus?.energyActiveImportRegisterValue) ?? 0
1863 );
1864 }
1865
1866 private getUseConnectorId0(stationTemplate?: ChargingStationTemplate): boolean {
1867 return stationTemplate?.useConnectorId0 ?? true;
1868 }
1869
1870 private async stopRunningTransactions(reason = StopTransactionReason.NONE): Promise<void> {
1871 if (this.hasEvses) {
1872 for (const evseStatus of this.evses.values()) {
1873 for (const [connectorId, connectorStatus] of evseStatus.connectors) {
1874 if (connectorStatus.transactionStarted === true) {
1875 await this.stopTransactionOnConnector(connectorId, reason);
1876 }
1877 }
1878 }
1879 } else {
1880 for (const connectorId of this.connectors.keys()) {
1881 if (connectorId > 0 && this.getConnectorStatus(connectorId)?.transactionStarted === true) {
1882 await this.stopTransactionOnConnector(connectorId, reason);
1883 }
1884 }
1885 }
1886 }
1887
1888 // 0 for disabling
1889 private getConnectionTimeout(): number {
1890 if (
1891 ChargingStationConfigurationUtils.getConfigurationKey(
1892 this,
1893 StandardParametersKey.ConnectionTimeOut
1894 )
1895 ) {
1896 return (
1897 parseInt(
1898 ChargingStationConfigurationUtils.getConfigurationKey(
1899 this,
1900 StandardParametersKey.ConnectionTimeOut
1901 ).value
1902 ) ?? Constants.DEFAULT_CONNECTION_TIMEOUT
1903 );
1904 }
1905 return Constants.DEFAULT_CONNECTION_TIMEOUT;
1906 }
1907
1908 // -1 for unlimited, 0 for disabling
1909 private getAutoReconnectMaxRetries(): number | undefined {
1910 return (
1911 this.stationInfo.autoReconnectMaxRetries ?? Configuration.getAutoReconnectMaxRetries() ?? -1
1912 );
1913 }
1914
1915 // 0 for disabling
1916 private getRegistrationMaxRetries(): number | undefined {
1917 return this.stationInfo.registrationMaxRetries ?? -1;
1918 }
1919
1920 private getPowerDivider(): number {
1921 let powerDivider = this.hasEvses ? this.getNumberOfEvses() : this.getNumberOfConnectors();
1922 if (this.stationInfo?.powerSharedByConnectors) {
1923 powerDivider = this.getNumberOfRunningTransactions();
1924 }
1925 return powerDivider;
1926 }
1927
1928 private getMaximumAmperage(stationInfo: ChargingStationInfo): number | undefined {
1929 const maximumPower = this.getMaximumPower(stationInfo);
1930 switch (this.getCurrentOutType(stationInfo)) {
1931 case CurrentType.AC:
1932 return ACElectricUtils.amperagePerPhaseFromPower(
1933 this.getNumberOfPhases(stationInfo),
1934 maximumPower / (this.hasEvses ? this.getNumberOfEvses() : this.getNumberOfConnectors()),
1935 this.getVoltageOut(stationInfo)
1936 );
1937 case CurrentType.DC:
1938 return DCElectricUtils.amperage(maximumPower, this.getVoltageOut(stationInfo));
1939 }
1940 }
1941
1942 private getAmperageLimitation(): number | undefined {
1943 if (
1944 Utils.isNotEmptyString(this.stationInfo?.amperageLimitationOcppKey) &&
1945 ChargingStationConfigurationUtils.getConfigurationKey(
1946 this,
1947 this.stationInfo.amperageLimitationOcppKey
1948 )
1949 ) {
1950 return (
1951 Utils.convertToInt(
1952 ChargingStationConfigurationUtils.getConfigurationKey(
1953 this,
1954 this.stationInfo.amperageLimitationOcppKey
1955 )?.value
1956 ) / ChargingStationUtils.getAmperageLimitationUnitDivider(this.stationInfo)
1957 );
1958 }
1959 }
1960
1961 private async startMessageSequence(): Promise<void> {
1962 if (this.stationInfo?.autoRegister === true) {
1963 await this.ocppRequestService.requestHandler<
1964 BootNotificationRequest,
1965 BootNotificationResponse
1966 >(this, RequestCommand.BOOT_NOTIFICATION, this.bootNotificationRequest, {
1967 skipBufferingOnError: true,
1968 });
1969 }
1970 // Start WebSocket ping
1971 this.startWebSocketPing();
1972 // Start heartbeat
1973 this.startHeartbeat();
1974 // Initialize connectors status
1975 if (this.hasEvses) {
1976 for (const [evseId, evseStatus] of this.evses) {
1977 if (evseId > 0) {
1978 for (const [connectorId, connectorStatus] of evseStatus.connectors) {
1979 const connectorBootStatus = ChargingStationUtils.getBootConnectorStatus(
1980 this,
1981 connectorId,
1982 connectorStatus
1983 );
1984 await OCPPServiceUtils.sendAndSetConnectorStatus(
1985 this,
1986 connectorId,
1987 connectorBootStatus
1988 );
1989 }
1990 }
1991 }
1992 } else {
1993 for (const connectorId of this.connectors.keys()) {
1994 if (connectorId > 0) {
1995 const connectorBootStatus = ChargingStationUtils.getBootConnectorStatus(
1996 this,
1997 connectorId,
1998 this.getConnectorStatus(connectorId)
1999 );
2000 await OCPPServiceUtils.sendAndSetConnectorStatus(this, connectorId, connectorBootStatus);
2001 }
2002 }
2003 }
2004 if (this.stationInfo?.firmwareStatus === FirmwareStatus.Installing) {
2005 await this.ocppRequestService.requestHandler<
2006 FirmwareStatusNotificationRequest,
2007 FirmwareStatusNotificationResponse
2008 >(this, RequestCommand.FIRMWARE_STATUS_NOTIFICATION, {
2009 status: FirmwareStatus.Installed,
2010 });
2011 this.stationInfo.firmwareStatus = FirmwareStatus.Installed;
2012 }
2013
2014 // Start the ATG
2015 if (this.getAutomaticTransactionGeneratorConfigurationFromTemplate()?.enable === true) {
2016 this.startAutomaticTransactionGenerator();
2017 }
2018 this.wsConnectionRestarted === true && this.flushMessageBuffer();
2019 }
2020
2021 private async stopMessageSequence(
2022 reason: StopTransactionReason = StopTransactionReason.NONE
2023 ): Promise<void> {
2024 // Stop WebSocket ping
2025 this.stopWebSocketPing();
2026 // Stop heartbeat
2027 this.stopHeartbeat();
2028 // Stop ongoing transactions
2029 if (this.automaticTransactionGenerator?.started === true) {
2030 this.stopAutomaticTransactionGenerator();
2031 } else {
2032 await this.stopRunningTransactions(reason);
2033 }
2034 if (this.hasEvses) {
2035 for (const [evseId, evseStatus] of this.evses) {
2036 if (evseId > 0) {
2037 for (const [connectorId, connectorStatus] of evseStatus.connectors) {
2038 await this.ocppRequestService.requestHandler<
2039 StatusNotificationRequest,
2040 StatusNotificationResponse
2041 >(
2042 this,
2043 RequestCommand.STATUS_NOTIFICATION,
2044 OCPPServiceUtils.buildStatusNotificationRequest(
2045 this,
2046 connectorId,
2047 ConnectorStatusEnum.Unavailable
2048 )
2049 );
2050 delete connectorStatus?.status;
2051 }
2052 }
2053 }
2054 } else {
2055 for (const connectorId of this.connectors.keys()) {
2056 if (connectorId > 0) {
2057 await this.ocppRequestService.requestHandler<
2058 StatusNotificationRequest,
2059 StatusNotificationResponse
2060 >(
2061 this,
2062 RequestCommand.STATUS_NOTIFICATION,
2063 OCPPServiceUtils.buildStatusNotificationRequest(
2064 this,
2065 connectorId,
2066 ConnectorStatusEnum.Unavailable
2067 )
2068 );
2069 delete this.getConnectorStatus(connectorId)?.status;
2070 }
2071 }
2072 }
2073 }
2074
2075 private startWebSocketPing(): void {
2076 const webSocketPingInterval: number = ChargingStationConfigurationUtils.getConfigurationKey(
2077 this,
2078 StandardParametersKey.WebSocketPingInterval
2079 )
2080 ? Utils.convertToInt(
2081 ChargingStationConfigurationUtils.getConfigurationKey(
2082 this,
2083 StandardParametersKey.WebSocketPingInterval
2084 )?.value
2085 )
2086 : 0;
2087 if (webSocketPingInterval > 0 && !this.webSocketPingSetInterval) {
2088 this.webSocketPingSetInterval = setInterval(() => {
2089 if (this.isWebSocketConnectionOpened() === true) {
2090 this.wsConnection?.ping();
2091 }
2092 }, webSocketPingInterval * 1000);
2093 logger.info(
2094 `${this.logPrefix()} WebSocket ping started every ${Utils.formatDurationSeconds(
2095 webSocketPingInterval
2096 )}`
2097 );
2098 } else if (this.webSocketPingSetInterval) {
2099 logger.info(
2100 `${this.logPrefix()} WebSocket ping already started every ${Utils.formatDurationSeconds(
2101 webSocketPingInterval
2102 )}`
2103 );
2104 } else {
2105 logger.error(
2106 `${this.logPrefix()} WebSocket ping interval set to ${webSocketPingInterval}, not starting the WebSocket ping`
2107 );
2108 }
2109 }
2110
2111 private stopWebSocketPing(): void {
2112 if (this.webSocketPingSetInterval) {
2113 clearInterval(this.webSocketPingSetInterval);
2114 delete this.webSocketPingSetInterval;
2115 }
2116 }
2117
2118 private getConfiguredSupervisionUrl(): URL {
2119 const supervisionUrls = this.stationInfo?.supervisionUrls ?? Configuration.getSupervisionUrls();
2120 if (Utils.isNotEmptyArray(supervisionUrls)) {
2121 let configuredSupervisionUrlIndex: number;
2122 switch (Configuration.getSupervisionUrlDistribution()) {
2123 case SupervisionUrlDistribution.RANDOM:
2124 configuredSupervisionUrlIndex = Math.floor(Utils.secureRandom() * supervisionUrls.length);
2125 break;
2126 case SupervisionUrlDistribution.ROUND_ROBIN:
2127 case SupervisionUrlDistribution.CHARGING_STATION_AFFINITY:
2128 default:
2129 Object.values(SupervisionUrlDistribution).includes(
2130 Configuration.getSupervisionUrlDistribution()
2131 ) === false &&
2132 logger.error(
2133 `${this.logPrefix()} Unknown supervision url distribution '${Configuration.getSupervisionUrlDistribution()}' from values '${SupervisionUrlDistribution.toString()}', defaulting to ${
2134 SupervisionUrlDistribution.CHARGING_STATION_AFFINITY
2135 }`
2136 );
2137 configuredSupervisionUrlIndex = (this.index - 1) % supervisionUrls.length;
2138 break;
2139 }
2140 return new URL(supervisionUrls[configuredSupervisionUrlIndex]);
2141 }
2142 return new URL(supervisionUrls as string);
2143 }
2144
2145 private stopHeartbeat(): void {
2146 if (this.heartbeatSetInterval) {
2147 clearInterval(this.heartbeatSetInterval);
2148 delete this.heartbeatSetInterval;
2149 }
2150 }
2151
2152 private terminateWSConnection(): void {
2153 if (this.isWebSocketConnectionOpened() === true) {
2154 this.wsConnection?.terminate();
2155 this.wsConnection = null;
2156 }
2157 }
2158
2159 private getReconnectExponentialDelay(): boolean {
2160 return this.stationInfo?.reconnectExponentialDelay ?? false;
2161 }
2162
2163 private async reconnect(): Promise<void> {
2164 // Stop WebSocket ping
2165 this.stopWebSocketPing();
2166 // Stop heartbeat
2167 this.stopHeartbeat();
2168 // Stop the ATG if needed
2169 if (this.automaticTransactionGenerator?.configuration?.stopOnConnectionFailure === true) {
2170 this.stopAutomaticTransactionGenerator();
2171 }
2172 if (
2173 this.autoReconnectRetryCount < this.getAutoReconnectMaxRetries() ||
2174 this.getAutoReconnectMaxRetries() === -1
2175 ) {
2176 this.autoReconnectRetryCount++;
2177 const reconnectDelay = this.getReconnectExponentialDelay()
2178 ? Utils.exponentialDelay(this.autoReconnectRetryCount)
2179 : this.getConnectionTimeout() * 1000;
2180 const reconnectDelayWithdraw = 1000;
2181 const reconnectTimeout =
2182 reconnectDelay && reconnectDelay - reconnectDelayWithdraw > 0
2183 ? reconnectDelay - reconnectDelayWithdraw
2184 : 0;
2185 logger.error(
2186 `${this.logPrefix()} WebSocket connection retry in ${Utils.roundTo(
2187 reconnectDelay,
2188 2
2189 )}ms, timeout ${reconnectTimeout}ms`
2190 );
2191 await Utils.sleep(reconnectDelay);
2192 logger.error(
2193 `${this.logPrefix()} WebSocket connection retry #${this.autoReconnectRetryCount.toString()}`
2194 );
2195 this.openWSConnection(
2196 {
2197 ...(this.stationInfo?.wsOptions ?? {}),
2198 handshakeTimeout: reconnectTimeout,
2199 },
2200 { closeOpened: true }
2201 );
2202 this.wsConnectionRestarted = true;
2203 } else if (this.getAutoReconnectMaxRetries() !== -1) {
2204 logger.error(
2205 `${this.logPrefix()} WebSocket connection retries failure: maximum retries reached (${
2206 this.autoReconnectRetryCount
2207 }) or retries disabled (${this.getAutoReconnectMaxRetries()})`
2208 );
2209 }
2210 }
2211
2212 private getAutomaticTransactionGeneratorConfigurationFromTemplate():
2213 | AutomaticTransactionGeneratorConfiguration
2214 | undefined {
2215 return this.getTemplateFromFile()?.AutomaticTransactionGenerator;
2216 }
2217 }