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