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