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