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