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