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