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