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