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