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