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