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