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