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