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