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