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