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