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