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