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