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