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