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