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