feat: add events for charging station status change
[e-mobility-charging-stations-simulator.git] / src / charging-station / ChargingStation.ts
CommitLineData
edd13439 1// Partial Copyright Jerome Benoit. 2021-2023. All Rights Reserved.
b4d34251 2
d972af76 3import { createHash } from 'node:crypto';
db54d2e0 4import { EventEmitter } from 'node:events';
0ebf7c2e 5import { type FSWatcher, existsSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs';
d972af76 6import { dirname, join } from 'node:path';
130783a7 7import { URL } from 'node:url';
01f4001e 8import { parentPort } from 'node:worker_threads';
8114d10e 9
be4c6702 10import { millisecondsToSeconds, secondsToMilliseconds } from 'date-fns';
5d280aae 11import merge from 'just-merge';
d972af76 12import { type RawData, WebSocket } from 'ws';
8114d10e 13
4c3c0d59 14import { AutomaticTransactionGenerator } from './AutomaticTransactionGenerator';
7671fa0b 15import { ChargingStationWorkerBroadcastChannel } from './broadcast-channel/ChargingStationWorkerBroadcastChannel';
f2d5e3d9
JB
16import {
17 addConfigurationKey,
18 deleteConfigurationKey,
19 getConfigurationKey,
20 setConfigurationKeyValue,
357a5553 21} from './ConfigurationKeyUtils';
fba11dc6
JB
22import {
23 buildConnectorsMap,
e6a33233 24 checkChargingStation,
fba11dc6
JB
25 checkConnectorsConfiguration,
26 checkStationInfoConnectorStatus,
27 checkTemplate,
fba11dc6
JB
28 createBootNotificationRequest,
29 createSerialNumber,
30 getAmperageLimitationUnitDivider,
31 getBootConnectorStatus,
32 getChargingStationConnectorChargingProfilesPowerLimit,
33 getChargingStationId,
34 getDefaultVoltageOut,
35 getHashId,
36 getIdTagsFile,
37 getMaxNumberOfEvses,
90aceaf6 38 getNumberOfReservableConnectors,
fba11dc6 39 getPhaseRotationValue,
d8093be1 40 hasFeatureProfile,
90aceaf6 41 hasReservationExpired,
fba11dc6
JB
42 initializeConnectorsMapStatus,
43 propagateSerialNumber,
90aceaf6 44 removeExpiredReservations,
fba11dc6
JB
45 stationTemplateToStationInfo,
46 warnTemplateKeysDeprecation,
08b58f00
JB
47} from './Helpers';
48import { IdTagsCache } from './IdTagsCache';
49import {
50 OCPP16IncomingRequestService,
51 OCPP16RequestService,
52 OCPP16ResponseService,
53 OCPP16ServiceUtils,
54 OCPP20IncomingRequestService,
55 OCPP20RequestService,
56 OCPP20ResponseService,
57 type OCPPIncomingRequestService,
58 type OCPPRequestService,
59 OCPPServiceUtils,
60} from './ocpp';
61import { SharedLRUCache } from './SharedLRUCache';
268a74bb 62import { BaseError, OCPPError } from '../exception';
b84bca85 63import { PerformanceStatistics } from '../performance';
e7aeea18 64import {
268a74bb 65 type AutomaticTransactionGeneratorConfiguration,
e7aeea18 66 AvailabilityType,
e0b0ee21 67 type BootNotificationRequest,
268a74bb 68 type BootNotificationResponse,
e0b0ee21 69 type CachedRequest,
268a74bb 70 type ChargingStationConfiguration,
db54d2e0 71 ChargingStationEvents,
268a74bb
JB
72 type ChargingStationInfo,
73 type ChargingStationOcppConfiguration,
74 type ChargingStationTemplate,
c8aafe0d 75 type ConnectorStatus,
268a74bb
JB
76 ConnectorStatusEnum,
77 CurrentType,
e0b0ee21 78 type ErrorCallback,
268a74bb
JB
79 type ErrorResponse,
80 ErrorType,
2585c6e9 81 type EvseStatus,
52952bf8 82 type EvseStatusConfiguration,
268a74bb 83 FileType,
c9a4f9ea
JB
84 FirmwareStatus,
85 type FirmwareStatusNotificationRequest,
268a74bb
JB
86 type FirmwareStatusNotificationResponse,
87 type FirmwareUpgrade,
e0b0ee21 88 type HeartbeatRequest,
268a74bb 89 type HeartbeatResponse,
e0b0ee21 90 type IncomingRequest,
268a74bb
JB
91 type IncomingRequestCommand,
92 type JsonType,
93 MessageType,
94 type MeterValue,
95 MeterValueMeasurand,
e0b0ee21 96 type MeterValuesRequest,
268a74bb
JB
97 type MeterValuesResponse,
98 OCPPVersion,
8ca6874c 99 type OutgoingRequest,
268a74bb
JB
100 PowerUnits,
101 RegistrationStatusEnumType,
e7aeea18 102 RequestCommand,
66dd3447 103 type Reservation,
366f75f6 104 type ReservationKey,
66dd3447 105 ReservationTerminationReason,
268a74bb 106 type Response,
268a74bb 107 StandardParametersKey,
5ced7e80 108 type Status,
e0b0ee21 109 type StatusNotificationRequest,
e0b0ee21 110 type StatusNotificationResponse,
ef6fa3fb 111 StopTransactionReason,
e0b0ee21
JB
112 type StopTransactionRequest,
113 type StopTransactionResponse,
268a74bb
JB
114 SupervisionUrlDistribution,
115 SupportedFeatureProfiles,
6dad8e21 116 VendorParametersKey,
268a74bb
JB
117 type WSError,
118 WebSocketCloseEventStatusCode,
119 type WsOptions,
120} from '../types';
60a74391
JB
121import {
122 ACElectricUtils,
1227a6f1
JB
123 AsyncLock,
124 AsyncLockType,
60a74391
JB
125 Configuration,
126 Constants,
127 DCElectricUtils,
179ed367
JB
128 buildChargingStationAutomaticTransactionGeneratorConfiguration,
129 buildConnectorsStatus,
130 buildEvsesStatus,
c8faabc8
JB
131 buildStartedMessage,
132 buildStoppedMessage,
133 buildUpdatedMessage,
9bf0ef23
JB
134 cloneObject,
135 convertToBoolean,
136 convertToInt,
137 exponentialDelay,
138 formatDurationMilliSeconds,
139 formatDurationSeconds,
140 getRandomInteger,
141 getWebSocketCloseEventStatusString,
fa5995d6 142 handleFileException,
9bf0ef23
JB
143 isNotEmptyArray,
144 isNotEmptyString,
145 isNullOrUndefined,
146 isUndefined,
147 logPrefix,
60a74391 148 logger,
5adf6ca4 149 min,
5f742aac 150 once,
9bf0ef23
JB
151 roundTo,
152 secureRandom,
153 sleep,
b3b3f0eb 154 // watchJsonFile,
60a74391 155} from '../utils';
3f40bc9c 156
db54d2e0 157export class ChargingStation extends EventEmitter {
c72f6634 158 public readonly index: number;
2484ac1e 159 public readonly templateFile: string;
6e0964c8 160 public stationInfo!: ChargingStationInfo;
452a82ca 161 public started: boolean;
cbf9b878 162 public starting: boolean;
f911a4af 163 public idTagsCache: IdTagsCache;
551e477c
JB
164 public automaticTransactionGenerator!: AutomaticTransactionGenerator | undefined;
165 public ocppConfiguration!: ChargingStationOcppConfiguration | undefined;
4ed03b6e 166 public wsConnection: WebSocket | null;
8df5ae48 167 public readonly connectors: Map<number, ConnectorStatus>;
2585c6e9 168 public readonly evses: Map<number, EvseStatus>;
9e23580d 169 public readonly requests: Map<string, CachedRequest>;
551e477c 170 public performanceStatistics!: PerformanceStatistics | undefined;
e1d9a0f4 171 public heartbeatSetInterval?: NodeJS.Timeout;
6e0964c8 172 public ocppRequestService!: OCPPRequestService;
0a03f36c 173 public bootNotificationRequest!: BootNotificationRequest;
1895299d 174 public bootNotificationResponse!: BootNotificationResponse | undefined;
fa7bccf4 175 public powerDivider!: number;
950b1349 176 private stopping: boolean;
073bd098 177 private configurationFile!: string;
7c72977b 178 private configurationFileHash!: string;
6e0964c8 179 private connectorsConfigurationHash!: string;
2585c6e9 180 private evsesConfigurationHash!: string;
c7db8ecb 181 private automaticTransactionGeneratorConfiguration?: AutomaticTransactionGeneratorConfiguration;
a472cf2b 182 private ocppIncomingRequestService!: OCPPIncomingRequestService;
8e242273 183 private readonly messageBuffer: Set<string>;
fa7bccf4 184 private configuredSupervisionUrl!: URL;
265e4266 185 private wsConnectionRestarted: boolean;
ad2f27c3 186 private autoReconnectRetryCount: number;
d972af76 187 private templateFileWatcher!: FSWatcher | undefined;
cda5d0fb 188 private templateFileHash!: string;
57adbebc 189 private readonly sharedLRUCache: SharedLRUCache;
e1d9a0f4 190 private webSocketPingSetInterval?: NodeJS.Timeout;
89b7a234 191 private readonly chargingStationWorkerBroadcastChannel: ChargingStationWorkerBroadcastChannel;
178956d8 192 private reservationExpirationSetInterval?: NodeJS.Timeout;
6af9012e 193
2484ac1e 194 constructor(index: number, templateFile: string) {
db54d2e0 195 super();
aa428a31 196 this.started = false;
950b1349
JB
197 this.starting = false;
198 this.stopping = false;
4ed03b6e 199 this.wsConnection = null;
aa428a31
JB
200 this.wsConnectionRestarted = false;
201 this.autoReconnectRetryCount = 0;
ad2f27c3 202 this.index = index;
2484ac1e 203 this.templateFile = templateFile;
9f2e3130 204 this.connectors = new Map<number, ConnectorStatus>();
2585c6e9 205 this.evses = new Map<number, EvseStatus>();
32b02249 206 this.requests = new Map<string, CachedRequest>();
8e242273 207 this.messageBuffer = new Set<string>();
b44b779a 208 this.sharedLRUCache = SharedLRUCache.getInstance();
f911a4af 209 this.idTagsCache = IdTagsCache.getInstance();
89b7a234 210 this.chargingStationWorkerBroadcastChannel = new ChargingStationWorkerBroadcastChannel(this);
32de5a57 211
db54d2e0
JB
212 this.on(ChargingStationEvents.started, () => {
213 parentPort?.postMessage(buildStartedMessage(this));
214 });
215 this.on(ChargingStationEvents.stopped, () => {
216 parentPort?.postMessage(buildStoppedMessage(this));
217 });
218 this.on(ChargingStationEvents.updated, () => {
219 parentPort?.postMessage(buildUpdatedMessage(this));
220 });
221
9f2e3130 222 this.initialize();
c0560973
JB
223 }
224
a14022a2
JB
225 public get hasEvses(): boolean {
226 return this.connectors.size === 0 && this.evses.size > 0;
227 }
228
25f5a959 229 private get wsConnectionUrl(): URL {
fa7bccf4 230 return new URL(
44eb6026 231 `${
269de583 232 this.getSupervisionUrlOcppConfiguration() &&
9bf0ef23 233 isNotEmptyString(this.getSupervisionUrlOcppKey()) &&
f2d5e3d9
JB
234 isNotEmptyString(getConfigurationKey(this, this.getSupervisionUrlOcppKey())?.value)
235 ? getConfigurationKey(this, this.getSupervisionUrlOcppKey())!.value
44eb6026 236 : this.configuredSupervisionUrl.href
5edd8ba0 237 }/${this.stationInfo.chargingStationId}`,
fa7bccf4 238 );
12fc74d6
JB
239 }
240
8b7072dc 241 public logPrefix = (): string => {
9bf0ef23 242 return logPrefix(
ccb1d6e9 243 ` ${
9bf0ef23 244 (isNotEmptyString(this?.stationInfo?.chargingStationId)
e302df1d 245 ? this?.stationInfo?.chargingStationId
e1d9a0f4 246 : getChargingStationId(this.index, this.getTemplateFromFile()!)) ??
e302df1d 247 'Error at building log prefix'
5edd8ba0 248 } |`,
ccb1d6e9 249 );
8b7072dc 250 };
c0560973 251
f911a4af 252 public hasIdTags(): boolean {
e1d9a0f4 253 return isNotEmptyArray(this.idTagsCache.getIdTags(getIdTagsFile(this.stationInfo)!));
c0560973
JB
254 }
255
ad774cec
JB
256 public getEnableStatistics(): boolean {
257 return this.stationInfo.enableStatistics ?? false;
c0560973
JB
258 }
259
cfdf901d
JB
260 public getRemoteAuthorization(): boolean {
261 return this.stationInfo.remoteAuthorization ?? true;
a7fc8211
JB
262 }
263
e1d9a0f4 264 public getNumberOfPhases(stationInfo?: ChargingStationInfo): number {
fa7bccf4
JB
265 const localStationInfo: ChargingStationInfo = stationInfo ?? this.stationInfo;
266 switch (this.getCurrentOutType(stationInfo)) {
4c2b4904 267 case CurrentType.AC:
e1d9a0f4 268 return !isUndefined(localStationInfo.numberOfPhases) ? localStationInfo.numberOfPhases! : 3;
4c2b4904 269 case CurrentType.DC:
c0560973
JB
270 return 0;
271 }
272 }
273
d5bff457 274 public isWebSocketConnectionOpened(): boolean {
0d8140bd 275 return this?.wsConnection?.readyState === WebSocket.OPEN;
c0560973
JB
276 }
277
1895299d 278 public getRegistrationStatus(): RegistrationStatusEnumType | undefined {
672fed6e
JB
279 return this?.bootNotificationResponse?.status;
280 }
281
f7c2994d 282 public inUnknownState(): boolean {
9bf0ef23 283 return isNullOrUndefined(this?.bootNotificationResponse?.status);
73c4266d
JB
284 }
285
f7c2994d 286 public inPendingState(): boolean {
d270cc87 287 return this?.bootNotificationResponse?.status === RegistrationStatusEnumType.PENDING;
16cd35ad
JB
288 }
289
f7c2994d 290 public inAcceptedState(): boolean {
d270cc87 291 return this?.bootNotificationResponse?.status === RegistrationStatusEnumType.ACCEPTED;
c0560973
JB
292 }
293
f7c2994d 294 public inRejectedState(): boolean {
d270cc87 295 return this?.bootNotificationResponse?.status === RegistrationStatusEnumType.REJECTED;
16cd35ad
JB
296 }
297
298 public isRegistered(): boolean {
ed6cfcff 299 return (
f7c2994d
JB
300 this.inUnknownState() === false &&
301 (this.inAcceptedState() === true || this.inPendingState() === true)
ed6cfcff 302 );
16cd35ad
JB
303 }
304
c0560973 305 public isChargingStationAvailable(): boolean {
0d6f335f 306 return this.getConnectorStatus(0)?.availability === AvailabilityType.Operative;
c0560973
JB
307 }
308
a14022a2
JB
309 public hasConnector(connectorId: number): boolean {
310 if (this.hasEvses) {
311 for (const evseStatus of this.evses.values()) {
312 if (evseStatus.connectors.has(connectorId)) {
313 return true;
314 }
315 }
316 return false;
317 }
318 return this.connectors.has(connectorId);
319 }
320
28e78158
JB
321 public isConnectorAvailable(connectorId: number): boolean {
322 return (
323 connectorId > 0 &&
324 this.getConnectorStatus(connectorId)?.availability === AvailabilityType.Operative
325 );
c0560973
JB
326 }
327
54544ef1 328 public getNumberOfConnectors(): number {
28e78158
JB
329 if (this.hasEvses) {
330 let numberOfConnectors = 0;
331 for (const [evseId, evseStatus] of this.evses) {
4334db72
JB
332 if (evseId > 0) {
333 numberOfConnectors += evseStatus.connectors.size;
28e78158 334 }
28e78158
JB
335 }
336 return numberOfConnectors;
337 }
007b5bde 338 return this.connectors.has(0) ? this.connectors.size - 1 : this.connectors.size;
54544ef1
JB
339 }
340
28e78158 341 public getNumberOfEvses(): number {
007b5bde 342 return this.evses.has(0) ? this.evses.size - 1 : this.evses.size;
28e78158
JB
343 }
344
345 public getConnectorStatus(connectorId: number): ConnectorStatus | undefined {
346 if (this.hasEvses) {
347 for (const evseStatus of this.evses.values()) {
348 if (evseStatus.connectors.has(connectorId)) {
349 return evseStatus.connectors.get(connectorId);
350 }
351 }
5e128725 352 return undefined;
28e78158
JB
353 }
354 return this.connectors.get(connectorId);
c0560973
JB
355 }
356
fa7bccf4 357 public getCurrentOutType(stationInfo?: ChargingStationInfo): CurrentType {
72092cfc 358 return (stationInfo ?? this.stationInfo)?.currentOutType ?? CurrentType.AC;
c0560973
JB
359 }
360
672fed6e 361 public getOcppStrictCompliance(): boolean {
0282b7c0 362 return this.stationInfo?.ocppStrictCompliance ?? true;
672fed6e
JB
363 }
364
e1d9a0f4 365 public getVoltageOut(stationInfo?: ChargingStationInfo): number {
fba11dc6 366 const defaultVoltageOut = getDefaultVoltageOut(
fa7bccf4 367 this.getCurrentOutType(stationInfo),
8a133cc8 368 this.logPrefix(),
5edd8ba0 369 this.templateFile,
492cf6ab 370 );
ac7f79af 371 return (stationInfo ?? this.stationInfo).voltageOut ?? defaultVoltageOut;
c0560973
JB
372 }
373
15068be9 374 public getMaximumPower(stationInfo?: ChargingStationInfo): number {
4fa476b7 375 return (stationInfo ?? this.stationInfo).maximumPower!;
15068be9
JB
376 }
377
ad8537a7 378 public getConnectorMaximumAvailablePower(connectorId: number): number {
e1d9a0f4 379 let connectorAmperageLimitationPowerLimit: number | undefined;
b47d68d7 380 if (
9bf0ef23 381 !isNullOrUndefined(this.getAmperageLimitation()) &&
e1d9a0f4 382 this.getAmperageLimitation()! < this.stationInfo.maximumAmperage!
b47d68d7 383 ) {
4160ae28
JB
384 connectorAmperageLimitationPowerLimit =
385 (this.getCurrentOutType() === CurrentType.AC
cc6e8ab5
JB
386 ? ACElectricUtils.powerTotal(
387 this.getNumberOfPhases(),
388 this.getVoltageOut(),
e1d9a0f4 389 this.getAmperageLimitation()! *
5edd8ba0 390 (this.hasEvses ? this.getNumberOfEvses() : this.getNumberOfConnectors()),
cc6e8ab5 391 )
e1d9a0f4 392 : DCElectricUtils.power(this.getVoltageOut(), this.getAmperageLimitation()!)) /
fa7bccf4 393 this.powerDivider;
cc6e8ab5 394 }
fa7bccf4 395 const connectorMaximumPower = this.getMaximumPower() / this.powerDivider;
15068be9 396 const connectorChargingProfilesPowerLimit =
fba11dc6 397 getChargingStationConnectorChargingProfilesPowerLimit(this, connectorId);
5adf6ca4 398 return min(
ad8537a7 399 isNaN(connectorMaximumPower) ? Infinity : connectorMaximumPower,
e1d9a0f4 400 isNaN(connectorAmperageLimitationPowerLimit!)
ad8537a7 401 ? Infinity
e1d9a0f4
JB
402 : connectorAmperageLimitationPowerLimit!,
403 isNaN(connectorChargingProfilesPowerLimit!) ? Infinity : connectorChargingProfilesPowerLimit!,
ad8537a7 404 );
cc6e8ab5
JB
405 }
406
6e0964c8 407 public getTransactionIdTag(transactionId: number): string | undefined {
28e78158
JB
408 if (this.hasEvses) {
409 for (const evseStatus of this.evses.values()) {
410 for (const connectorStatus of evseStatus.connectors.values()) {
411 if (connectorStatus.transactionId === transactionId) {
412 return connectorStatus.transactionIdTag;
413 }
414 }
415 }
416 } else {
417 for (const connectorId of this.connectors.keys()) {
3fa7f799 418 if (this.getConnectorStatus(connectorId)?.transactionId === transactionId) {
28e78158
JB
419 return this.getConnectorStatus(connectorId)?.transactionIdTag;
420 }
c0560973
JB
421 }
422 }
423 }
424
ded57f02 425 public getNumberOfRunningTransactions(): number {
cfc9875a 426 let numberOfRunningTransactions = 0;
ded57f02 427 if (this.hasEvses) {
3fa7f799
JB
428 for (const [evseId, evseStatus] of this.evses) {
429 if (evseId === 0) {
430 continue;
431 }
ded57f02
JB
432 for (const connectorStatus of evseStatus.connectors.values()) {
433 if (connectorStatus.transactionStarted === true) {
cfc9875a 434 ++numberOfRunningTransactions;
ded57f02
JB
435 }
436 }
437 }
438 } else {
439 for (const connectorId of this.connectors.keys()) {
440 if (connectorId > 0 && this.getConnectorStatus(connectorId)?.transactionStarted === true) {
cfc9875a 441 ++numberOfRunningTransactions;
ded57f02
JB
442 }
443 }
444 }
cfc9875a 445 return numberOfRunningTransactions;
ded57f02
JB
446 }
447
6ed92bc1 448 public getOutOfOrderEndMeterValues(): boolean {
ccb1d6e9 449 return this.stationInfo?.outOfOrderEndMeterValues ?? false;
6ed92bc1
JB
450 }
451
452 public getBeginEndMeterValues(): boolean {
ccb1d6e9 453 return this.stationInfo?.beginEndMeterValues ?? false;
6ed92bc1
JB
454 }
455
456 public getMeteringPerTransaction(): boolean {
ccb1d6e9 457 return this.stationInfo?.meteringPerTransaction ?? true;
6ed92bc1
JB
458 }
459
fd0c36fa 460 public getTransactionDataMeterValues(): boolean {
ccb1d6e9 461 return this.stationInfo?.transactionDataMeterValues ?? false;
fd0c36fa
JB
462 }
463
9ccca265 464 public getMainVoltageMeterValues(): boolean {
ccb1d6e9 465 return this.stationInfo?.mainVoltageMeterValues ?? true;
9ccca265
JB
466 }
467
6b10669b 468 public getPhaseLineToLineVoltageMeterValues(): boolean {
ccb1d6e9 469 return this.stationInfo?.phaseLineToLineVoltageMeterValues ?? false;
9bd87386
JB
470 }
471
7bc31f9c 472 public getCustomValueLimitationMeterValues(): boolean {
ccb1d6e9 473 return this.stationInfo?.customValueLimitationMeterValues ?? true;
7bc31f9c
JB
474 }
475
f479a792 476 public getConnectorIdByTransactionId(transactionId: number): number | undefined {
28e78158
JB
477 if (this.hasEvses) {
478 for (const evseStatus of this.evses.values()) {
479 for (const [connectorId, connectorStatus] of evseStatus.connectors) {
480 if (connectorStatus.transactionId === transactionId) {
481 return connectorId;
482 }
483 }
484 }
485 } else {
486 for (const connectorId of this.connectors.keys()) {
3fa7f799 487 if (this.getConnectorStatus(connectorId)?.transactionId === transactionId) {
28e78158
JB
488 return connectorId;
489 }
c0560973
JB
490 }
491 }
492 }
493
07989fad
JB
494 public getEnergyActiveImportRegisterByTransactionId(
495 transactionId: number,
5edd8ba0 496 rounded = false,
07989fad
JB
497 ): number {
498 return this.getEnergyActiveImportRegister(
e1d9a0f4 499 this.getConnectorStatus(this.getConnectorIdByTransactionId(transactionId)!)!,
5edd8ba0 500 rounded,
cbad1217 501 );
cbad1217
JB
502 }
503
18bf8274 504 public getEnergyActiveImportRegisterByConnectorId(connectorId: number, rounded = false): number {
e1d9a0f4 505 return this.getEnergyActiveImportRegister(this.getConnectorStatus(connectorId)!, rounded);
6ed92bc1
JB
506 }
507
c0560973 508 public getAuthorizeRemoteTxRequests(): boolean {
f2d5e3d9 509 const authorizeRemoteTxRequests = getConfigurationKey(
17ac262c 510 this,
5edd8ba0 511 StandardParametersKey.AuthorizeRemoteTxRequests,
e7aeea18 512 );
9bf0ef23 513 return authorizeRemoteTxRequests ? convertToBoolean(authorizeRemoteTxRequests.value) : false;
c0560973
JB
514 }
515
516 public getLocalAuthListEnabled(): boolean {
f2d5e3d9 517 const localAuthListEnabled = getConfigurationKey(
17ac262c 518 this,
5edd8ba0 519 StandardParametersKey.LocalAuthListEnabled,
e7aeea18 520 );
9bf0ef23 521 return localAuthListEnabled ? convertToBoolean(localAuthListEnabled.value) : false;
c0560973
JB
522 }
523
8f953431 524 public getHeartbeatInterval(): number {
f2d5e3d9 525 const HeartbeatInterval = getConfigurationKey(this, StandardParametersKey.HeartbeatInterval);
8f953431 526 if (HeartbeatInterval) {
be4c6702 527 return secondsToMilliseconds(convertToInt(HeartbeatInterval.value));
8f953431 528 }
f2d5e3d9 529 const HeartBeatInterval = getConfigurationKey(this, StandardParametersKey.HeartBeatInterval);
8f953431 530 if (HeartBeatInterval) {
be4c6702 531 return secondsToMilliseconds(convertToInt(HeartBeatInterval.value));
8f953431
JB
532 }
533 this.stationInfo?.autoRegister === false &&
534 logger.warn(
535 `${this.logPrefix()} Heartbeat interval configuration key not set, using default value: ${
536 Constants.DEFAULT_HEARTBEAT_INTERVAL
5edd8ba0 537 }`,
8f953431
JB
538 );
539 return Constants.DEFAULT_HEARTBEAT_INTERVAL;
540 }
541
269de583
JB
542 public setSupervisionUrl(url: string): void {
543 if (
544 this.getSupervisionUrlOcppConfiguration() &&
9bf0ef23 545 isNotEmptyString(this.getSupervisionUrlOcppKey())
269de583 546 ) {
f2d5e3d9 547 setConfigurationKeyValue(this, this.getSupervisionUrlOcppKey(), url);
269de583
JB
548 } else {
549 this.stationInfo.supervisionUrls = url;
550 this.saveStationInfo();
551 this.configuredSupervisionUrl = this.getConfiguredSupervisionUrl();
552 }
553 }
554
c0560973 555 public startHeartbeat(): void {
5b43123f 556 if (this.getHeartbeatInterval() > 0 && !this.heartbeatSetInterval) {
6a8329b4
JB
557 this.heartbeatSetInterval = setInterval(() => {
558 this.ocppRequestService
559 .requestHandler<HeartbeatRequest, HeartbeatResponse>(this, RequestCommand.HEARTBEAT)
72092cfc 560 .catch((error) => {
6a8329b4
JB
561 logger.error(
562 `${this.logPrefix()} Error while sending '${RequestCommand.HEARTBEAT}':`,
5edd8ba0 563 error,
6a8329b4
JB
564 );
565 });
c0560973 566 }, this.getHeartbeatInterval());
e7aeea18 567 logger.info(
9bf0ef23 568 `${this.logPrefix()} Heartbeat started every ${formatDurationMilliSeconds(
5edd8ba0
JB
569 this.getHeartbeatInterval(),
570 )}`,
e7aeea18 571 );
c0560973 572 } else if (this.heartbeatSetInterval) {
e7aeea18 573 logger.info(
9bf0ef23 574 `${this.logPrefix()} Heartbeat already started every ${formatDurationMilliSeconds(
5edd8ba0
JB
575 this.getHeartbeatInterval(),
576 )}`,
e7aeea18 577 );
c0560973 578 } else {
e7aeea18 579 logger.error(
944d4529 580 `${this.logPrefix()} Heartbeat interval set to ${this.getHeartbeatInterval()}, not starting the heartbeat`,
e7aeea18 581 );
c0560973
JB
582 }
583 }
584
585 public restartHeartbeat(): void {
586 // Stop heartbeat
587 this.stopHeartbeat();
588 // Start heartbeat
589 this.startHeartbeat();
590 }
591
17ac262c
JB
592 public restartWebSocketPing(): void {
593 // Stop WebSocket ping
594 this.stopWebSocketPing();
595 // Start WebSocket ping
596 this.startWebSocketPing();
597 }
598
c0560973
JB
599 public startMeterValues(connectorId: number, interval: number): void {
600 if (connectorId === 0) {
e7aeea18 601 logger.error(
04c32a95 602 `${this.logPrefix()} Trying to start MeterValues on connector id ${connectorId}`,
e7aeea18 603 );
c0560973
JB
604 return;
605 }
734d790d 606 if (!this.getConnectorStatus(connectorId)) {
e7aeea18 607 logger.error(
66dd3447 608 `${this.logPrefix()} Trying to start MeterValues on non existing connector id
04c32a95 609 ${connectorId}`,
e7aeea18 610 );
c0560973
JB
611 return;
612 }
5e3cb728 613 if (this.getConnectorStatus(connectorId)?.transactionStarted === false) {
e7aeea18 614 logger.error(
944d4529 615 `${this.logPrefix()} Trying to start MeterValues on connector id ${connectorId} with no transaction started`,
e7aeea18 616 );
c0560973 617 return;
e7aeea18 618 } else if (
5e3cb728 619 this.getConnectorStatus(connectorId)?.transactionStarted === true &&
9bf0ef23 620 isNullOrUndefined(this.getConnectorStatus(connectorId)?.transactionId)
e7aeea18
JB
621 ) {
622 logger.error(
944d4529 623 `${this.logPrefix()} Trying to start MeterValues on connector id ${connectorId} with no transaction id`,
e7aeea18 624 );
c0560973
JB
625 return;
626 }
627 if (interval > 0) {
e1d9a0f4 628 this.getConnectorStatus(connectorId)!.transactionSetInterval = setInterval(() => {
6a8329b4
JB
629 // FIXME: Implement OCPP version agnostic helpers
630 const meterValue: MeterValue = OCPP16ServiceUtils.buildMeterValue(
631 this,
632 connectorId,
e1d9a0f4 633 this.getConnectorStatus(connectorId)!.transactionId!,
5edd8ba0 634 interval,
6a8329b4
JB
635 );
636 this.ocppRequestService
637 .requestHandler<MeterValuesRequest, MeterValuesResponse>(
08f130a0 638 this,
f22266fd
JB
639 RequestCommand.METER_VALUES,
640 {
641 connectorId,
72092cfc 642 transactionId: this.getConnectorStatus(connectorId)?.transactionId,
f22266fd 643 meterValue: [meterValue],
5edd8ba0 644 },
6a8329b4 645 )
72092cfc 646 .catch((error) => {
6a8329b4
JB
647 logger.error(
648 `${this.logPrefix()} Error while sending '${RequestCommand.METER_VALUES}':`,
5edd8ba0 649 error,
6a8329b4
JB
650 );
651 });
652 }, interval);
c0560973 653 } else {
e7aeea18
JB
654 logger.error(
655 `${this.logPrefix()} Charging station ${
656 StandardParametersKey.MeterValueSampleInterval
5edd8ba0 657 } configuration set to ${interval}, not sending MeterValues`,
e7aeea18 658 );
c0560973
JB
659 }
660 }
661
04b1261c
JB
662 public stopMeterValues(connectorId: number) {
663 if (this.getConnectorStatus(connectorId)?.transactionSetInterval) {
664 clearInterval(this.getConnectorStatus(connectorId)?.transactionSetInterval);
665 }
666 }
667
c0560973 668 public start(): void {
0d8852a5
JB
669 if (this.started === false) {
670 if (this.starting === false) {
671 this.starting = true;
ad774cec 672 if (this.getEnableStatistics() === true) {
551e477c 673 this.performanceStatistics?.start();
0d8852a5 674 }
d8093be1 675 if (hasFeatureProfile(this, SupportedFeatureProfiles.Reservation)) {
178956d8 676 this.startReservationExpirationSetInterval();
d193a949 677 }
0d8852a5
JB
678 this.openWSConnection();
679 // Monitor charging station template file
b3b3f0eb
JB
680 // FIXME: Disabled until the spurious configuration file change detection is identified
681 // this.templateFileWatcher = watchJsonFile(
682 // this.templateFile,
683 // FileType.ChargingStationTemplate,
684 // this.logPrefix(),
685 // undefined,
686 // (event, filename): void => {
687 // if (isNotEmptyString(filename) && event === 'change') {
688 // try {
689 // logger.debug(
690 // `${this.logPrefix()} ${FileType.ChargingStationTemplate} ${
691 // this.templateFile
692 // } file have changed, reload`,
693 // );
694 // this.sharedLRUCache.deleteChargingStationTemplate(this.templateFileHash);
695 // // Initialize
696 // this.initialize();
697 // this.idTagsCache.deleteIdTags(getIdTagsFile(this.stationInfo)!);
698 // // Restart the ATG
699 // this.stopAutomaticTransactionGenerator()
700 // .then(() => {
701 // delete this.automaticTransactionGeneratorConfiguration;
702 // if (this.getAutomaticTransactionGeneratorConfiguration()?.enable === true) {
703 // this.startAutomaticTransactionGenerator();
704 // }
705 // })
706 // .catch((err) =>
707 // logger.error(
708 // `${this.logPrefix()} failed to stop ATG at ${
709 // FileType.ChargingStationTemplate
710 // } reload`,
711 // err,
712 // ),
713 // );
714 // if (this.getEnableStatistics() === true) {
715 // this.performanceStatistics?.restart();
716 // } else {
717 // this.performanceStatistics?.stop();
718 // }
719 // // FIXME?: restart heartbeat and WebSocket ping when their interval values have changed
720 // } catch (error) {
721 // logger.error(
722 // `${this.logPrefix()} ${FileType.ChargingStationTemplate} file monitoring error:`,
723 // error,
724 // );
725 // }
726 // }
727 // },
728 // );
56eb297e 729 this.started = true;
db54d2e0 730 this.emit(ChargingStationEvents.started);
0d8852a5
JB
731 this.starting = false;
732 } else {
733 logger.warn(`${this.logPrefix()} Charging station is already starting...`);
734 }
950b1349 735 } else {
0d8852a5 736 logger.warn(`${this.logPrefix()} Charging station is already started...`);
950b1349 737 }
c0560973
JB
738 }
739
9ff486f4 740 public async stop(reason?: StopTransactionReason, stopTransactions?: boolean): Promise<void> {
0d8852a5
JB
741 if (this.started === true) {
742 if (this.stopping === false) {
743 this.stopping = true;
9ff486f4 744 await this.stopMessageSequence(reason, stopTransactions);
0d8852a5 745 this.closeWSConnection();
ad774cec 746 if (this.getEnableStatistics() === true) {
551e477c 747 this.performanceStatistics?.stop();
0d8852a5 748 }
d8093be1 749 if (hasFeatureProfile(this, SupportedFeatureProfiles.Reservation)) {
5543b88d
JB
750 this.stopReservationExpirationSetInterval();
751 }
0d8852a5 752 this.sharedLRUCache.deleteChargingStationConfiguration(this.configurationFileHash);
72092cfc 753 this.templateFileWatcher?.close();
cda5d0fb 754 this.sharedLRUCache.deleteChargingStationTemplate(this.templateFileHash);
cdde2cfe 755 delete this.bootNotificationResponse;
0d8852a5 756 this.started = false;
ac7f79af 757 this.saveConfiguration();
db54d2e0 758 this.emit(ChargingStationEvents.stopped);
0d8852a5
JB
759 this.stopping = false;
760 } else {
761 logger.warn(`${this.logPrefix()} Charging station is already stopping...`);
c0560973 762 }
950b1349 763 } else {
0d8852a5 764 logger.warn(`${this.logPrefix()} Charging station is already stopped...`);
c0560973 765 }
c0560973
JB
766 }
767
60ddad53
JB
768 public async reset(reason?: StopTransactionReason): Promise<void> {
769 await this.stop(reason);
e1d9a0f4 770 await sleep(this.stationInfo.resetTime!);
fa7bccf4 771 this.initialize();
94ec7e96
JB
772 this.start();
773 }
774
17ac262c
JB
775 public saveOcppConfiguration(): void {
776 if (this.getOcppPersistentConfiguration()) {
b1bbdae5 777 this.saveConfiguration();
e6895390
JB
778 }
779 }
780
8e242273
JB
781 public bufferMessage(message: string): void {
782 this.messageBuffer.add(message);
3ba2381e
JB
783 }
784
db2336d9 785 public openWSConnection(
7f3decca
JB
786 options?: WsOptions,
787 params?: { closeOpened?: boolean; terminateOpened?: boolean },
db2336d9 788 ): void {
7f3decca
JB
789 options = {
790 handshakeTimeout: secondsToMilliseconds(this.getConnectionTimeout()),
e6a33233 791 ...this.stationInfo?.wsOptions,
7f3decca
JB
792 ...options,
793 };
b1bbdae5 794 params = { ...{ closeOpened: false, terminateOpened: false }, ...params };
e6a33233 795 if (!checkChargingStation(this, this.logPrefix())) {
d1c6c833
JB
796 return;
797 }
db2336d9 798 if (
9bf0ef23
JB
799 !isNullOrUndefined(this.stationInfo.supervisionUser) &&
800 !isNullOrUndefined(this.stationInfo.supervisionPassword)
db2336d9
JB
801 ) {
802 options.auth = `${this.stationInfo.supervisionUser}:${this.stationInfo.supervisionPassword}`;
803 }
804 if (params?.closeOpened) {
805 this.closeWSConnection();
806 }
807 if (params?.terminateOpened) {
808 this.terminateWSConnection();
809 }
db2336d9 810
56eb297e 811 if (this.isWebSocketConnectionOpened() === true) {
0a03f36c 812 logger.warn(
944d4529 813 `${this.logPrefix()} OCPP connection to URL ${this.wsConnectionUrl.toString()} is already opened`,
0a03f36c
JB
814 );
815 return;
816 }
817
db2336d9 818 logger.info(
5edd8ba0 819 `${this.logPrefix()} Open OCPP connection to URL ${this.wsConnectionUrl.toString()}`,
db2336d9
JB
820 );
821
feff11ec
JB
822 this.wsConnection = new WebSocket(
823 this.wsConnectionUrl,
824 `ocpp${this.stationInfo.ocppVersion ?? OCPPVersion.VERSION_16}`,
5edd8ba0 825 options,
feff11ec 826 );
db2336d9
JB
827
828 // Handle WebSocket message
829 this.wsConnection.on(
830 'message',
5edd8ba0 831 this.onMessage.bind(this) as (this: WebSocket, data: RawData, isBinary: boolean) => void,
db2336d9
JB
832 );
833 // Handle WebSocket error
834 this.wsConnection.on(
835 'error',
5edd8ba0 836 this.onError.bind(this) as (this: WebSocket, error: Error) => void,
db2336d9
JB
837 );
838 // Handle WebSocket close
839 this.wsConnection.on(
840 'close',
5edd8ba0 841 this.onClose.bind(this) as (this: WebSocket, code: number, reason: Buffer) => void,
db2336d9
JB
842 );
843 // Handle WebSocket open
844 this.wsConnection.on('open', this.onOpen.bind(this) as (this: WebSocket) => void);
845 // Handle WebSocket ping
846 this.wsConnection.on('ping', this.onPing.bind(this) as (this: WebSocket, data: Buffer) => void);
847 // Handle WebSocket pong
848 this.wsConnection.on('pong', this.onPong.bind(this) as (this: WebSocket, data: Buffer) => void);
849 }
850
851 public closeWSConnection(): void {
56eb297e 852 if (this.isWebSocketConnectionOpened() === true) {
72092cfc 853 this.wsConnection?.close();
db2336d9
JB
854 this.wsConnection = null;
855 }
856 }
857
e1d9a0f4 858 public getAutomaticTransactionGeneratorConfiguration(): AutomaticTransactionGeneratorConfiguration {
c7db8ecb
JB
859 if (isNullOrUndefined(this.automaticTransactionGeneratorConfiguration)) {
860 let automaticTransactionGeneratorConfiguration:
861 | AutomaticTransactionGeneratorConfiguration
862 | undefined;
863 const automaticTransactionGeneratorConfigurationFromFile =
864 this.getConfigurationFromFile()?.automaticTransactionGenerator;
865 if (
866 this.getAutomaticTransactionGeneratorPersistentConfiguration() &&
867 automaticTransactionGeneratorConfigurationFromFile
868 ) {
869 automaticTransactionGeneratorConfiguration =
870 automaticTransactionGeneratorConfigurationFromFile;
871 } else {
872 automaticTransactionGeneratorConfiguration =
873 this.getTemplateFromFile()?.AutomaticTransactionGenerator;
874 }
875 this.automaticTransactionGeneratorConfiguration = {
876 ...Constants.DEFAULT_ATG_CONFIGURATION,
877 ...automaticTransactionGeneratorConfiguration,
878 };
ac7f79af 879 }
c7db8ecb 880 return this.automaticTransactionGeneratorConfiguration!;
ac7f79af
JB
881 }
882
5ced7e80
JB
883 public getAutomaticTransactionGeneratorStatuses(): Status[] | undefined {
884 return this.getConfigurationFromFile()?.automaticTransactionGeneratorStatuses;
885 }
886
ac7f79af
JB
887 public startAutomaticTransactionGenerator(connectorIds?: number[]): void {
888 this.automaticTransactionGenerator = AutomaticTransactionGenerator.getInstance(this);
9bf0ef23 889 if (isNotEmptyArray(connectorIds)) {
e1d9a0f4 890 for (const connectorId of connectorIds!) {
551e477c 891 this.automaticTransactionGenerator?.startConnector(connectorId);
a5e9befc
JB
892 }
893 } else {
551e477c 894 this.automaticTransactionGenerator?.start();
4f69be04 895 }
cb60061f 896 this.saveAutomaticTransactionGeneratorConfiguration();
db54d2e0 897 this.emit(ChargingStationEvents.updated);
4f69be04
JB
898 }
899
9ff486f4 900 public stopAutomaticTransactionGenerator(connectorIds?: number[]): void {
9bf0ef23 901 if (isNotEmptyArray(connectorIds)) {
e1d9a0f4 902 for (const connectorId of connectorIds!) {
9ff486f4 903 this.automaticTransactionGenerator?.stopConnector(connectorId);
a5e9befc
JB
904 }
905 } else {
9ff486f4 906 this.automaticTransactionGenerator?.stop();
4f69be04 907 }
cb60061f 908 this.saveAutomaticTransactionGeneratorConfiguration();
db54d2e0 909 this.emit(ChargingStationEvents.updated);
4f69be04
JB
910 }
911
5e3cb728
JB
912 public async stopTransactionOnConnector(
913 connectorId: number,
9ff486f4 914 reason?: StopTransactionReason,
5e3cb728 915 ): Promise<StopTransactionResponse> {
72092cfc 916 const transactionId = this.getConnectorStatus(connectorId)?.transactionId;
5e3cb728 917 if (
c7e8e0a2
JB
918 this.getBeginEndMeterValues() === true &&
919 this.getOcppStrictCompliance() === true &&
920 this.getOutOfOrderEndMeterValues() === false
5e3cb728
JB
921 ) {
922 // FIXME: Implement OCPP version agnostic helpers
923 const transactionEndMeterValue = OCPP16ServiceUtils.buildTransactionEndMeterValue(
924 this,
925 connectorId,
e1d9a0f4 926 this.getEnergyActiveImportRegisterByTransactionId(transactionId!),
5e3cb728
JB
927 );
928 await this.ocppRequestService.requestHandler<MeterValuesRequest, MeterValuesResponse>(
929 this,
930 RequestCommand.METER_VALUES,
931 {
932 connectorId,
933 transactionId,
934 meterValue: [transactionEndMeterValue],
5edd8ba0 935 },
5e3cb728
JB
936 );
937 }
938 return this.ocppRequestService.requestHandler<StopTransactionRequest, StopTransactionResponse>(
939 this,
940 RequestCommand.STOP_TRANSACTION,
941 {
942 transactionId,
e1d9a0f4 943 meterStop: this.getEnergyActiveImportRegisterByTransactionId(transactionId!, true),
9ff486f4 944 ...(isNullOrUndefined(reason) && { reason }),
5edd8ba0 945 },
5e3cb728
JB
946 );
947 }
948
10e8c3e1 949 public getReserveConnectorZeroSupported(): boolean {
9bf0ef23 950 return convertToBoolean(
f2d5e3d9 951 getConfigurationKey(this, StandardParametersKey.ReserveConnectorZeroSupported)!.value,
24578c31
JB
952 );
953 }
954
d193a949 955 public async addReservation(reservation: Reservation): Promise<void> {
2ca0ea90 956 const reservationFound = this.getReservationBy('reservationId', reservation.reservationId);
530e5fbb
JB
957 if (!isUndefined(reservationFound)) {
958 await this.removeReservation(
959 reservationFound!,
960 ReservationTerminationReason.REPLACE_EXISTING,
961 );
d193a949 962 }
e1d9a0f4 963 this.getConnectorStatus(reservation.connectorId)!.reservation = reservation;
ec94a3cf 964 await OCPPServiceUtils.sendAndSetConnectorStatus(
d193a949 965 this,
ec94a3cf
JB
966 reservation.connectorId,
967 ConnectorStatusEnum.Reserved,
e1d9a0f4 968 undefined,
5edd8ba0 969 { send: reservation.connectorId !== 0 },
d193a949 970 );
24578c31
JB
971 }
972
d193a949
JB
973 public async removeReservation(
974 reservation: Reservation,
90aceaf6 975 reason: ReservationTerminationReason,
d193a949 976 ): Promise<void> {
e1d9a0f4 977 const connector = this.getConnectorStatus(reservation.connectorId)!;
d193a949 978 switch (reason) {
96d96b12 979 case ReservationTerminationReason.CONNECTOR_STATE_CHANGED:
ec94a3cf 980 case ReservationTerminationReason.TRANSACTION_STARTED:
ec9f36cc
JB
981 delete connector.reservation;
982 break;
e74bc549
JB
983 case ReservationTerminationReason.RESERVATION_CANCELED:
984 case ReservationTerminationReason.REPLACE_EXISTING:
985 case ReservationTerminationReason.EXPIRED:
ec94a3cf 986 await OCPPServiceUtils.sendAndSetConnectorStatus(
d193a949 987 this,
ec94a3cf
JB
988 reservation.connectorId,
989 ConnectorStatusEnum.Available,
e1d9a0f4 990 undefined,
5edd8ba0 991 { send: reservation.connectorId !== 0 },
d193a949 992 );
ec94a3cf 993 delete connector.reservation;
d193a949 994 break;
b029e74e 995 default:
90aceaf6 996 // eslint-disable-next-line @typescript-eslint/restrict-template-expressions
56563a3c 997 throw new BaseError(`Unknown reservation termination reason '${reason}'`);
d193a949 998 }
24578c31
JB
999 }
1000
3fa7f799 1001 public getReservationBy(
366f75f6 1002 filterKey: ReservationKey,
5edd8ba0 1003 value: number | string,
3fa7f799 1004 ): Reservation | undefined {
66dd3447 1005 if (this.hasEvses) {
3fa7f799
JB
1006 for (const evseStatus of this.evses.values()) {
1007 for (const connectorStatus of evseStatus.connectors.values()) {
2ca0ea90 1008 if (connectorStatus?.reservation?.[filterKey] === value) {
3fa7f799 1009 return connectorStatus.reservation;
66dd3447
JB
1010 }
1011 }
1012 }
1013 } else {
3fa7f799 1014 for (const connectorStatus of this.connectors.values()) {
2ca0ea90 1015 if (connectorStatus?.reservation?.[filterKey] === value) {
3fa7f799 1016 return connectorStatus.reservation;
66dd3447
JB
1017 }
1018 }
1019 }
d193a949
JB
1020 }
1021
e6948a57
JB
1022 public isConnectorReservable(
1023 reservationId: number,
1024 idTag?: string,
1025 connectorId?: number,
1026 ): boolean {
90aceaf6
JB
1027 const reservation = this.getReservationBy('reservationId', reservationId);
1028 const reservationExists = !isUndefined(reservation) && !hasReservationExpired(reservation!);
e6948a57
JB
1029 if (arguments.length === 1) {
1030 return !reservationExists;
1031 } else if (arguments.length > 1) {
90aceaf6
JB
1032 const userReservation = !isUndefined(idTag)
1033 ? this.getReservationBy('idTag', idTag!)
1034 : undefined;
e6948a57 1035 const userReservationExists =
90aceaf6 1036 !isUndefined(userReservation) && !hasReservationExpired(userReservation!);
e6948a57
JB
1037 const notConnectorZero = isUndefined(connectorId) ? true : connectorId! > 0;
1038 const freeConnectorsAvailable = this.getNumberOfReservableConnectors() > 0;
1039 return (
1040 !reservationExists && !userReservationExists && notConnectorZero && freeConnectorsAvailable
1041 );
1042 }
1043 return false;
1044 }
1045
1046 private startReservationExpirationSetInterval(customInterval?: number): void {
2035255d 1047 const interval = customInterval ?? Constants.DEFAULT_RESERVATION_EXPIRATION_INTERVAL;
42371a2e 1048 if (interval > 0) {
04c32a95
JB
1049 logger.info(
1050 `${this.logPrefix()} Reservation expiration date checks started every ${formatDurationMilliSeconds(
1051 interval,
1052 )}`,
1053 );
37aa4e56 1054 this.reservationExpirationSetInterval = setInterval((): void => {
90aceaf6 1055 removeExpiredReservations(this).catch(Constants.EMPTY_FUNCTION);
42371a2e
JB
1056 }, interval);
1057 }
d193a949
JB
1058 }
1059
e6948a57 1060 private stopReservationExpirationSetInterval(): void {
d116b9b5 1061 if (!isNullOrUndefined(this.reservationExpirationSetInterval)) {
e6948a57
JB
1062 clearInterval(this.reservationExpirationSetInterval);
1063 }
d193a949
JB
1064 }
1065
366f75f6
JB
1066 // private restartReservationExpiryDateSetInterval(): void {
1067 // this.stopReservationExpirationSetInterval();
1068 // this.startReservationExpirationSetInterval();
1069 // }
d193a949
JB
1070
1071 private getNumberOfReservableConnectors(): number {
90aceaf6 1072 let numberOfReservableConnectors = 0;
66dd3447 1073 if (this.hasEvses) {
3fa7f799 1074 for (const evseStatus of this.evses.values()) {
90aceaf6 1075 numberOfReservableConnectors += getNumberOfReservableConnectors(evseStatus.connectors);
66dd3447
JB
1076 }
1077 } else {
90aceaf6 1078 numberOfReservableConnectors = getNumberOfReservableConnectors(this.connectors);
66dd3447 1079 }
90aceaf6 1080 return numberOfReservableConnectors - this.getNumberOfReservationsOnConnectorZero();
66dd3447
JB
1081 }
1082
d193a949 1083 private getNumberOfReservationsOnConnectorZero(): number {
6913d568
JB
1084 if (
1085 // eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing
1086 (this.hasEvses && this.evses.get(0)?.connectors.get(0)?.reservation) ||
1087 (!this.hasEvses && this.connectors.get(0)?.reservation)
1088 ) {
cfc9875a 1089 return 1;
66dd3447 1090 }
cfc9875a 1091 return 0;
24578c31
JB
1092 }
1093
f90c1757 1094 private flushMessageBuffer(): void {
8e242273 1095 if (this.messageBuffer.size > 0) {
7d3b0f64 1096 for (const message of this.messageBuffer.values()) {
e1d9a0f4
JB
1097 let beginId: string | undefined;
1098 let commandName: RequestCommand | undefined;
8ca6874c 1099 const [messageType] = JSON.parse(message) as OutgoingRequest | Response | ErrorResponse;
1431af78
JB
1100 const isRequest = messageType === MessageType.CALL_MESSAGE;
1101 if (isRequest) {
1102 [, , commandName] = JSON.parse(message) as OutgoingRequest;
1103 beginId = PerformanceStatistics.beginMeasure(commandName);
1104 }
72092cfc 1105 this.wsConnection?.send(message);
e1d9a0f4 1106 isRequest && PerformanceStatistics.endMeasure(commandName!, beginId!);
8ca6874c
JB
1107 logger.debug(
1108 `${this.logPrefix()} >> Buffered ${OCPPServiceUtils.getMessageTypeString(
5edd8ba0
JB
1109 messageType,
1110 )} payload sent: ${message}`,
8ca6874c 1111 );
8e242273 1112 this.messageBuffer.delete(message);
7d3b0f64 1113 }
77f00f84
JB
1114 }
1115 }
1116
1f5df42a
JB
1117 private getSupervisionUrlOcppConfiguration(): boolean {
1118 return this.stationInfo.supervisionUrlOcppConfiguration ?? false;
12fc74d6
JB
1119 }
1120
e8e865ea 1121 private getSupervisionUrlOcppKey(): string {
6dad8e21 1122 return this.stationInfo.supervisionUrlOcppKey ?? VendorParametersKey.ConnectionUrl;
e8e865ea
JB
1123 }
1124
72092cfc 1125 private getTemplateFromFile(): ChargingStationTemplate | undefined {
e1d9a0f4 1126 let template: ChargingStationTemplate | undefined;
5ad8570f 1127 try {
cda5d0fb
JB
1128 if (this.sharedLRUCache.hasChargingStationTemplate(this.templateFileHash)) {
1129 template = this.sharedLRUCache.getChargingStationTemplate(this.templateFileHash);
7c72977b
JB
1130 } else {
1131 const measureId = `${FileType.ChargingStationTemplate} read`;
1132 const beginId = PerformanceStatistics.beginMeasure(measureId);
d972af76 1133 template = JSON.parse(readFileSync(this.templateFile, 'utf8')) as ChargingStationTemplate;
7c72977b 1134 PerformanceStatistics.endMeasure(measureId, beginId);
d972af76 1135 template.templateHash = createHash(Constants.DEFAULT_HASH_ALGORITHM)
7c72977b
JB
1136 .update(JSON.stringify(template))
1137 .digest('hex');
57adbebc 1138 this.sharedLRUCache.setChargingStationTemplate(template);
cda5d0fb 1139 this.templateFileHash = template.templateHash;
7c72977b 1140 }
5ad8570f 1141 } catch (error) {
fa5995d6 1142 handleFileException(
2484ac1e 1143 this.templateFile,
7164966d
JB
1144 FileType.ChargingStationTemplate,
1145 error as NodeJS.ErrnoException,
5edd8ba0 1146 this.logPrefix(),
e7aeea18 1147 );
5ad8570f 1148 }
2484ac1e
JB
1149 return template;
1150 }
1151
7a3a2ebb 1152 private getStationInfoFromTemplate(): ChargingStationInfo {
e1d9a0f4 1153 const stationTemplate: ChargingStationTemplate = this.getTemplateFromFile()!;
fba11dc6 1154 checkTemplate(stationTemplate, this.logPrefix(), this.templateFile);
5f742aac
JB
1155 const warnTemplateKeysDeprecationOnce = once(warnTemplateKeysDeprecation, this);
1156 warnTemplateKeysDeprecationOnce(stationTemplate, this.logPrefix(), this.templateFile);
8a133cc8 1157 if (stationTemplate?.Connectors) {
fba11dc6 1158 checkConnectorsConfiguration(stationTemplate, this.logPrefix(), this.templateFile);
8a133cc8 1159 }
fba11dc6
JB
1160 const stationInfo: ChargingStationInfo = stationTemplateToStationInfo(stationTemplate);
1161 stationInfo.hashId = getHashId(this.index, stationTemplate);
1162 stationInfo.chargingStationId = getChargingStationId(this.index, stationTemplate);
72092cfc 1163 stationInfo.ocppVersion = stationTemplate?.ocppVersion ?? OCPPVersion.VERSION_16;
fba11dc6 1164 createSerialNumber(stationTemplate, stationInfo);
9bf0ef23 1165 if (isNotEmptyArray(stationTemplate?.power)) {
551e477c 1166 stationTemplate.power = stationTemplate.power as number[];
9bf0ef23 1167 const powerArrayRandomIndex = Math.floor(secureRandom() * stationTemplate.power.length);
cc6e8ab5 1168 stationInfo.maximumPower =
72092cfc 1169 stationTemplate?.powerUnit === PowerUnits.KILO_WATT
fa7bccf4
JB
1170 ? stationTemplate.power[powerArrayRandomIndex] * 1000
1171 : stationTemplate.power[powerArrayRandomIndex];
5ad8570f 1172 } else {
551e477c 1173 stationTemplate.power = stationTemplate?.power as number;
cc6e8ab5 1174 stationInfo.maximumPower =
72092cfc 1175 stationTemplate?.powerUnit === PowerUnits.KILO_WATT
fa7bccf4
JB
1176 ? stationTemplate.power * 1000
1177 : stationTemplate.power;
1178 }
3637ca2c 1179 stationInfo.firmwareVersionPattern =
72092cfc 1180 stationTemplate?.firmwareVersionPattern ?? Constants.SEMVER_PATTERN;
3637ca2c 1181 if (
9bf0ef23 1182 isNotEmptyString(stationInfo.firmwareVersion) &&
e1d9a0f4 1183 new RegExp(stationInfo.firmwareVersionPattern).test(stationInfo.firmwareVersion!) === false
3637ca2c
JB
1184 ) {
1185 logger.warn(
1186 `${this.logPrefix()} Firmware version '${stationInfo.firmwareVersion}' in template file ${
1187 this.templateFile
5edd8ba0 1188 } does not match firmware version pattern '${stationInfo.firmwareVersionPattern}'`,
3637ca2c
JB
1189 );
1190 }
598c886d 1191 stationInfo.firmwareUpgrade = merge<FirmwareUpgrade>(
15748260 1192 {
598c886d
JB
1193 versionUpgrade: {
1194 step: 1,
1195 },
15748260
JB
1196 reset: true,
1197 },
5edd8ba0 1198 stationTemplate?.firmwareUpgrade ?? {},
15748260 1199 );
9bf0ef23 1200 stationInfo.resetTime = !isNullOrUndefined(stationTemplate?.resetTime)
be4c6702 1201 ? secondsToMilliseconds(stationTemplate.resetTime!)
e7aeea18 1202 : Constants.CHARGING_STATION_DEFAULT_RESET_TIME;
fa7bccf4 1203 stationInfo.maximumAmperage = this.getMaximumAmperage(stationInfo);
9ac86a7e 1204 return stationInfo;
5ad8570f
JB
1205 }
1206
551e477c
JB
1207 private getStationInfoFromFile(): ChargingStationInfo | undefined {
1208 let stationInfo: ChargingStationInfo | undefined;
f832e5df
JB
1209 if (this.getStationInfoPersistentConfiguration()) {
1210 stationInfo = this.getConfigurationFromFile()?.stationInfo;
1211 if (stationInfo) {
1212 delete stationInfo?.infoHash;
1213 }
1214 }
f765beaa 1215 return stationInfo;
2484ac1e
JB
1216 }
1217
1218 private getStationInfo(): ChargingStationInfo {
1219 const stationInfoFromTemplate: ChargingStationInfo = this.getStationInfoFromTemplate();
551e477c 1220 const stationInfoFromFile: ChargingStationInfo | undefined = this.getStationInfoFromFile();
6b90dcca
JB
1221 // Priority:
1222 // 1. charging station info from template
1223 // 2. charging station info from configuration file
f765beaa 1224 if (stationInfoFromFile?.templateHash === stationInfoFromTemplate.templateHash) {
e1d9a0f4 1225 return stationInfoFromFile!;
f765beaa 1226 }
fec4d204 1227 stationInfoFromFile &&
fba11dc6 1228 propagateSerialNumber(
e1d9a0f4 1229 this.getTemplateFromFile()!,
fec4d204 1230 stationInfoFromFile,
5edd8ba0 1231 stationInfoFromTemplate,
fec4d204 1232 );
01efc60a 1233 return stationInfoFromTemplate;
2484ac1e
JB
1234 }
1235
1236 private saveStationInfo(): void {
ccb1d6e9 1237 if (this.getStationInfoPersistentConfiguration()) {
b1bbdae5 1238 this.saveConfiguration();
ccb1d6e9 1239 }
2484ac1e
JB
1240 }
1241
e8e865ea 1242 private getOcppPersistentConfiguration(): boolean {
ccb1d6e9
JB
1243 return this.stationInfo?.ocppPersistentConfiguration ?? true;
1244 }
1245
1246 private getStationInfoPersistentConfiguration(): boolean {
1247 return this.stationInfo?.stationInfoPersistentConfiguration ?? true;
e8e865ea
JB
1248 }
1249
5ced7e80
JB
1250 private getAutomaticTransactionGeneratorPersistentConfiguration(): boolean {
1251 return this.stationInfo?.automaticTransactionGeneratorPersistentConfiguration ?? true;
1252 }
1253
c0560973 1254 private handleUnsupportedVersion(version: OCPPVersion) {
944d4529 1255 const errorMsg = `Unsupported protocol version '${version}' configured in template file ${this.templateFile}`;
ded57f02
JB
1256 logger.error(`${this.logPrefix()} ${errorMsg}`);
1257 throw new BaseError(errorMsg);
c0560973
JB
1258 }
1259
2484ac1e 1260 private initialize(): void {
e1d9a0f4 1261 const stationTemplate = this.getTemplateFromFile()!;
fba11dc6 1262 checkTemplate(stationTemplate, this.logPrefix(), this.templateFile);
d972af76
JB
1263 this.configurationFile = join(
1264 dirname(this.templateFile.replace('station-templates', 'configurations')),
5edd8ba0 1265 `${getHashId(this.index, stationTemplate)}.json`,
0642c3d2 1266 );
a4f7c75f 1267 const chargingStationConfiguration = this.getConfigurationFromFile();
a4f7c75f 1268 if (
ba01a213 1269 chargingStationConfiguration?.stationInfo?.templateHash === stationTemplate?.templateHash &&
e1d9a0f4 1270 // eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing
a4f7c75f
JB
1271 (chargingStationConfiguration?.connectorsStatus || chargingStationConfiguration?.evsesStatus)
1272 ) {
1273 this.initializeConnectorsOrEvsesFromFile(chargingStationConfiguration);
1274 } else {
1275 this.initializeConnectorsOrEvsesFromTemplate(stationTemplate);
1276 }
b44b779a 1277 this.stationInfo = this.getStationInfo();
3637ca2c
JB
1278 if (
1279 this.stationInfo.firmwareStatus === FirmwareStatus.Installing &&
9bf0ef23
JB
1280 isNotEmptyString(this.stationInfo.firmwareVersion) &&
1281 isNotEmptyString(this.stationInfo.firmwareVersionPattern)
3637ca2c 1282 ) {
d812bdcb 1283 const patternGroup: number | undefined =
15748260 1284 this.stationInfo.firmwareUpgrade?.versionUpgrade?.patternGroup ??
d812bdcb 1285 this.stationInfo.firmwareVersion?.split('.').length;
e1d9a0f4
JB
1286 const match = this.stationInfo
1287 .firmwareVersion!.match(new RegExp(this.stationInfo.firmwareVersionPattern!))!
1288 .slice(1, patternGroup! + 1);
3637ca2c 1289 const patchLevelIndex = match.length - 1;
5d280aae 1290 match[patchLevelIndex] = (
9bf0ef23 1291 convertToInt(match[patchLevelIndex]) +
e1d9a0f4 1292 this.stationInfo.firmwareUpgrade!.versionUpgrade!.step!
5d280aae 1293 ).toString();
72092cfc 1294 this.stationInfo.firmwareVersion = match?.join('.');
3637ca2c 1295 }
6bccfcbc 1296 this.saveStationInfo();
6bccfcbc
JB
1297 this.configuredSupervisionUrl = this.getConfiguredSupervisionUrl();
1298 if (this.getEnableStatistics() === true) {
1299 this.performanceStatistics = PerformanceStatistics.getInstance(
1300 this.stationInfo.hashId,
e1d9a0f4 1301 this.stationInfo.chargingStationId!,
5edd8ba0 1302 this.configuredSupervisionUrl,
6bccfcbc
JB
1303 );
1304 }
fba11dc6 1305 this.bootNotificationRequest = createBootNotificationRequest(this.stationInfo);
692f2f64
JB
1306 this.powerDivider = this.getPowerDivider();
1307 // OCPP configuration
1308 this.ocppConfiguration = this.getOcppConfiguration();
1309 this.initializeOcppConfiguration();
1310 this.initializeOcppServices();
1311 if (this.stationInfo?.autoRegister === true) {
1312 this.bootNotificationResponse = {
1313 currentTime: new Date(),
be4c6702 1314 interval: millisecondsToSeconds(this.getHeartbeatInterval()),
692f2f64
JB
1315 status: RegistrationStatusEnumType.ACCEPTED,
1316 };
1317 }
147d0e0f
JB
1318 }
1319
feff11ec
JB
1320 private initializeOcppServices(): void {
1321 const ocppVersion = this.stationInfo.ocppVersion ?? OCPPVersion.VERSION_16;
1322 switch (ocppVersion) {
1323 case OCPPVersion.VERSION_16:
1324 this.ocppIncomingRequestService =
1325 OCPP16IncomingRequestService.getInstance<OCPP16IncomingRequestService>();
1326 this.ocppRequestService = OCPP16RequestService.getInstance<OCPP16RequestService>(
5edd8ba0 1327 OCPP16ResponseService.getInstance<OCPP16ResponseService>(),
feff11ec
JB
1328 );
1329 break;
1330 case OCPPVersion.VERSION_20:
1331 case OCPPVersion.VERSION_201:
1332 this.ocppIncomingRequestService =
1333 OCPP20IncomingRequestService.getInstance<OCPP20IncomingRequestService>();
1334 this.ocppRequestService = OCPP20RequestService.getInstance<OCPP20RequestService>(
5edd8ba0 1335 OCPP20ResponseService.getInstance<OCPP20ResponseService>(),
feff11ec
JB
1336 );
1337 break;
1338 default:
1339 this.handleUnsupportedVersion(ocppVersion);
1340 break;
1341 }
1342 }
1343
2484ac1e 1344 private initializeOcppConfiguration(): void {
f2d5e3d9
JB
1345 if (!getConfigurationKey(this, StandardParametersKey.HeartbeatInterval)) {
1346 addConfigurationKey(this, StandardParametersKey.HeartbeatInterval, '0');
f0f65a62 1347 }
f2d5e3d9
JB
1348 if (!getConfigurationKey(this, StandardParametersKey.HeartBeatInterval)) {
1349 addConfigurationKey(this, StandardParametersKey.HeartBeatInterval, '0', { visible: false });
f0f65a62 1350 }
e7aeea18
JB
1351 if (
1352 this.getSupervisionUrlOcppConfiguration() &&
9bf0ef23 1353 isNotEmptyString(this.getSupervisionUrlOcppKey()) &&
f2d5e3d9 1354 !getConfigurationKey(this, this.getSupervisionUrlOcppKey())
e7aeea18 1355 ) {
f2d5e3d9 1356 addConfigurationKey(
17ac262c 1357 this,
a59737e3 1358 this.getSupervisionUrlOcppKey(),
fa7bccf4 1359 this.configuredSupervisionUrl.href,
5edd8ba0 1360 { reboot: true },
e7aeea18 1361 );
e6895390
JB
1362 } else if (
1363 !this.getSupervisionUrlOcppConfiguration() &&
9bf0ef23 1364 isNotEmptyString(this.getSupervisionUrlOcppKey()) &&
f2d5e3d9 1365 getConfigurationKey(this, this.getSupervisionUrlOcppKey())
e6895390 1366 ) {
f2d5e3d9 1367 deleteConfigurationKey(this, this.getSupervisionUrlOcppKey(), { save: false });
12fc74d6 1368 }
cc6e8ab5 1369 if (
9bf0ef23 1370 isNotEmptyString(this.stationInfo?.amperageLimitationOcppKey) &&
f2d5e3d9 1371 !getConfigurationKey(this, this.stationInfo.amperageLimitationOcppKey!)
cc6e8ab5 1372 ) {
f2d5e3d9 1373 addConfigurationKey(
17ac262c 1374 this,
e1d9a0f4 1375 this.stationInfo.amperageLimitationOcppKey!,
17ac262c 1376 (
e1d9a0f4 1377 this.stationInfo.maximumAmperage! * getAmperageLimitationUnitDivider(this.stationInfo)
5edd8ba0 1378 ).toString(),
cc6e8ab5
JB
1379 );
1380 }
f2d5e3d9
JB
1381 if (!getConfigurationKey(this, StandardParametersKey.SupportedFeatureProfiles)) {
1382 addConfigurationKey(
17ac262c 1383 this,
e7aeea18 1384 StandardParametersKey.SupportedFeatureProfiles,
5edd8ba0 1385 `${SupportedFeatureProfiles.Core},${SupportedFeatureProfiles.FirmwareManagement},${SupportedFeatureProfiles.LocalAuthListManagement},${SupportedFeatureProfiles.SmartCharging},${SupportedFeatureProfiles.RemoteTrigger}`,
e7aeea18
JB
1386 );
1387 }
f2d5e3d9 1388 addConfigurationKey(
17ac262c 1389 this,
e7aeea18
JB
1390 StandardParametersKey.NumberOfConnectors,
1391 this.getNumberOfConnectors().toString(),
a95873d8 1392 { readonly: true },
5edd8ba0 1393 { overwrite: true },
e7aeea18 1394 );
f2d5e3d9
JB
1395 if (!getConfigurationKey(this, StandardParametersKey.MeterValuesSampledData)) {
1396 addConfigurationKey(
17ac262c 1397 this,
e7aeea18 1398 StandardParametersKey.MeterValuesSampledData,
5edd8ba0 1399 MeterValueMeasurand.ENERGY_ACTIVE_IMPORT_REGISTER,
e7aeea18 1400 );
7abfea5f 1401 }
f2d5e3d9 1402 if (!getConfigurationKey(this, StandardParametersKey.ConnectorPhaseRotation)) {
dd08d43d 1403 const connectorsPhaseRotation: string[] = [];
28e78158
JB
1404 if (this.hasEvses) {
1405 for (const evseStatus of this.evses.values()) {
1406 for (const connectorId of evseStatus.connectors.keys()) {
dd08d43d 1407 connectorsPhaseRotation.push(
e1d9a0f4 1408 getPhaseRotationValue(connectorId, this.getNumberOfPhases())!,
dd08d43d 1409 );
28e78158
JB
1410 }
1411 }
1412 } else {
1413 for (const connectorId of this.connectors.keys()) {
dd08d43d 1414 connectorsPhaseRotation.push(
e1d9a0f4 1415 getPhaseRotationValue(connectorId, this.getNumberOfPhases())!,
dd08d43d 1416 );
7e1dc878
JB
1417 }
1418 }
f2d5e3d9 1419 addConfigurationKey(
17ac262c 1420 this,
e7aeea18 1421 StandardParametersKey.ConnectorPhaseRotation,
5edd8ba0 1422 connectorsPhaseRotation.toString(),
e7aeea18 1423 );
7e1dc878 1424 }
f2d5e3d9
JB
1425 if (!getConfigurationKey(this, StandardParametersKey.AuthorizeRemoteTxRequests)) {
1426 addConfigurationKey(this, StandardParametersKey.AuthorizeRemoteTxRequests, 'true');
36f6a92e 1427 }
17ac262c 1428 if (
f2d5e3d9
JB
1429 !getConfigurationKey(this, StandardParametersKey.LocalAuthListEnabled) &&
1430 getConfigurationKey(this, StandardParametersKey.SupportedFeatureProfiles)?.value?.includes(
1431 SupportedFeatureProfiles.LocalAuthListManagement,
17ac262c
JB
1432 )
1433 ) {
f2d5e3d9
JB
1434 addConfigurationKey(this, StandardParametersKey.LocalAuthListEnabled, 'false');
1435 }
1436 if (!getConfigurationKey(this, StandardParametersKey.ConnectionTimeOut)) {
1437 addConfigurationKey(
17ac262c 1438 this,
e7aeea18 1439 StandardParametersKey.ConnectionTimeOut,
5edd8ba0 1440 Constants.DEFAULT_CONNECTION_TIMEOUT.toString(),
e7aeea18 1441 );
8bce55bf 1442 }
2484ac1e 1443 this.saveOcppConfiguration();
073bd098
JB
1444 }
1445
a4f7c75f
JB
1446 private initializeConnectorsOrEvsesFromFile(configuration: ChargingStationConfiguration): void {
1447 if (configuration?.connectorsStatus && !configuration?.evsesStatus) {
8df5ae48 1448 for (const [connectorId, connectorStatus] of configuration.connectorsStatus.entries()) {
9bf0ef23 1449 this.connectors.set(connectorId, cloneObject<ConnectorStatus>(connectorStatus));
8df5ae48 1450 }
a4f7c75f
JB
1451 } else if (configuration?.evsesStatus && !configuration?.connectorsStatus) {
1452 for (const [evseId, evseStatusConfiguration] of configuration.evsesStatus.entries()) {
9bf0ef23 1453 const evseStatus = cloneObject<EvseStatusConfiguration>(evseStatusConfiguration);
a4f7c75f
JB
1454 delete evseStatus.connectorsStatus;
1455 this.evses.set(evseId, {
8df5ae48 1456 ...(evseStatus as EvseStatus),
a4f7c75f 1457 connectors: new Map<number, ConnectorStatus>(
e1d9a0f4 1458 evseStatusConfiguration.connectorsStatus!.map((connectorStatus, connectorId) => [
a4f7c75f
JB
1459 connectorId,
1460 connectorStatus,
5edd8ba0 1461 ]),
a4f7c75f
JB
1462 ),
1463 });
1464 }
1465 } else if (configuration?.evsesStatus && configuration?.connectorsStatus) {
1466 const errorMsg = `Connectors and evses defined at the same time in configuration file ${this.configurationFile}`;
1467 logger.error(`${this.logPrefix()} ${errorMsg}`);
1468 throw new BaseError(errorMsg);
1469 } else {
1470 const errorMsg = `No connectors or evses defined in configuration file ${this.configurationFile}`;
1471 logger.error(`${this.logPrefix()} ${errorMsg}`);
1472 throw new BaseError(errorMsg);
1473 }
1474 }
1475
34eeb1fb 1476 private initializeConnectorsOrEvsesFromTemplate(stationTemplate: ChargingStationTemplate) {
cda5d0fb 1477 if (stationTemplate?.Connectors && !stationTemplate?.Evses) {
34eeb1fb 1478 this.initializeConnectorsFromTemplate(stationTemplate);
cda5d0fb 1479 } else if (stationTemplate?.Evses && !stationTemplate?.Connectors) {
34eeb1fb 1480 this.initializeEvsesFromTemplate(stationTemplate);
cda5d0fb 1481 } else if (stationTemplate?.Evses && stationTemplate?.Connectors) {
ae25f265
JB
1482 const errorMsg = `Connectors and evses defined at the same time in template file ${this.templateFile}`;
1483 logger.error(`${this.logPrefix()} ${errorMsg}`);
1484 throw new BaseError(errorMsg);
1485 } else {
1486 const errorMsg = `No connectors or evses defined in template file ${this.templateFile}`;
1487 logger.error(`${this.logPrefix()} ${errorMsg}`);
1488 throw new BaseError(errorMsg);
1489 }
1490 }
1491
34eeb1fb 1492 private initializeConnectorsFromTemplate(stationTemplate: ChargingStationTemplate): void {
cda5d0fb 1493 if (!stationTemplate?.Connectors && this.connectors.size === 0) {
ded57f02
JB
1494 const errorMsg = `No already defined connectors and charging station information from template ${this.templateFile} with no connectors configuration defined`;
1495 logger.error(`${this.logPrefix()} ${errorMsg}`);
1496 throw new BaseError(errorMsg);
3d25cc86 1497 }
e1d9a0f4 1498 if (!stationTemplate?.Connectors?.[0]) {
3d25cc86
JB
1499 logger.warn(
1500 `${this.logPrefix()} Charging station information from template ${
1501 this.templateFile
5edd8ba0 1502 } with no connector id 0 configuration`,
3d25cc86
JB
1503 );
1504 }
cda5d0fb
JB
1505 if (stationTemplate?.Connectors) {
1506 const { configuredMaxConnectors, templateMaxConnectors, templateMaxAvailableConnectors } =
fba11dc6 1507 checkConnectorsConfiguration(stationTemplate, this.logPrefix(), this.templateFile);
d972af76 1508 const connectorsConfigHash = createHash(Constants.DEFAULT_HASH_ALGORITHM)
cda5d0fb 1509 .update(
5edd8ba0 1510 `${JSON.stringify(stationTemplate?.Connectors)}${configuredMaxConnectors.toString()}`,
cda5d0fb 1511 )
3d25cc86
JB
1512 .digest('hex');
1513 const connectorsConfigChanged =
1514 this.connectors?.size !== 0 && this.connectorsConfigurationHash !== connectorsConfigHash;
1515 if (this.connectors?.size === 0 || connectorsConfigChanged) {
1516 connectorsConfigChanged && this.connectors.clear();
1517 this.connectorsConfigurationHash = connectorsConfigHash;
269196a8
JB
1518 if (templateMaxConnectors > 0) {
1519 for (let connectorId = 0; connectorId <= configuredMaxConnectors; connectorId++) {
1520 if (
1521 connectorId === 0 &&
1c9de2b9 1522 (!stationTemplate?.Connectors?.[connectorId] ||
cda5d0fb 1523 this.getUseConnectorId0(stationTemplate) === false)
269196a8
JB
1524 ) {
1525 continue;
1526 }
1527 const templateConnectorId =
cda5d0fb 1528 connectorId > 0 && stationTemplate?.randomConnectors
9bf0ef23 1529 ? getRandomInteger(templateMaxAvailableConnectors, 1)
269196a8 1530 : connectorId;
cda5d0fb 1531 const connectorStatus = stationTemplate?.Connectors[templateConnectorId];
fba11dc6 1532 checkStationInfoConnectorStatus(
ae25f265 1533 templateConnectorId,
04b1261c
JB
1534 connectorStatus,
1535 this.logPrefix(),
5edd8ba0 1536 this.templateFile,
04b1261c 1537 );
9bf0ef23 1538 this.connectors.set(connectorId, cloneObject<ConnectorStatus>(connectorStatus));
3d25cc86 1539 }
fba11dc6 1540 initializeConnectorsMapStatus(this.connectors, this.logPrefix());
52952bf8 1541 this.saveConnectorsStatus();
ae25f265
JB
1542 } else {
1543 logger.warn(
1544 `${this.logPrefix()} Charging station information from template ${
1545 this.templateFile
5edd8ba0 1546 } with no connectors configuration defined, cannot create connectors`,
ae25f265 1547 );
3d25cc86
JB
1548 }
1549 }
1550 } else {
1551 logger.warn(
1552 `${this.logPrefix()} Charging station information from template ${
1553 this.templateFile
5edd8ba0 1554 } with no connectors configuration defined, using already defined connectors`,
3d25cc86
JB
1555 );
1556 }
3d25cc86
JB
1557 }
1558
34eeb1fb 1559 private initializeEvsesFromTemplate(stationTemplate: ChargingStationTemplate): void {
cda5d0fb 1560 if (!stationTemplate?.Evses && this.evses.size === 0) {
ded57f02
JB
1561 const errorMsg = `No already defined evses and charging station information from template ${this.templateFile} with no evses configuration defined`;
1562 logger.error(`${this.logPrefix()} ${errorMsg}`);
1563 throw new BaseError(errorMsg);
2585c6e9 1564 }
e1d9a0f4 1565 if (!stationTemplate?.Evses?.[0]) {
2585c6e9
JB
1566 logger.warn(
1567 `${this.logPrefix()} Charging station information from template ${
1568 this.templateFile
5edd8ba0 1569 } with no evse id 0 configuration`,
2585c6e9
JB
1570 );
1571 }
e1d9a0f4 1572 if (!stationTemplate?.Evses?.[0]?.Connectors?.[0]) {
59a0f26d
JB
1573 logger.warn(
1574 `${this.logPrefix()} Charging station information from template ${
1575 this.templateFile
5edd8ba0 1576 } with evse id 0 with no connector id 0 configuration`,
59a0f26d
JB
1577 );
1578 }
491dad29
JB
1579 if (Object.keys(stationTemplate?.Evses?.[0]?.Connectors as object).length > 1) {
1580 logger.warn(
1581 `${this.logPrefix()} Charging station information from template ${
1582 this.templateFile
1583 } with evse id 0 with more than one connector configuration, only connector id 0 configuration will be used`,
1584 );
1585 }
cda5d0fb 1586 if (stationTemplate?.Evses) {
d972af76 1587 const evsesConfigHash = createHash(Constants.DEFAULT_HASH_ALGORITHM)
ba01a213 1588 .update(JSON.stringify(stationTemplate?.Evses))
2585c6e9
JB
1589 .digest('hex');
1590 const evsesConfigChanged =
1591 this.evses?.size !== 0 && this.evsesConfigurationHash !== evsesConfigHash;
1592 if (this.evses?.size === 0 || evsesConfigChanged) {
1593 evsesConfigChanged && this.evses.clear();
1594 this.evsesConfigurationHash = evsesConfigHash;
fba11dc6 1595 const templateMaxEvses = getMaxNumberOfEvses(stationTemplate?.Evses);
ae25f265 1596 if (templateMaxEvses > 0) {
eb979012
JB
1597 for (const evseKey in stationTemplate.Evses) {
1598 const evseId = convertToInt(evseKey);
52952bf8 1599 this.evses.set(evseId, {
fba11dc6 1600 connectors: buildConnectorsMap(
eb979012 1601 stationTemplate?.Evses[evseKey]?.Connectors,
ae25f265 1602 this.logPrefix(),
5edd8ba0 1603 this.templateFile,
ae25f265
JB
1604 ),
1605 availability: AvailabilityType.Operative,
1606 });
e1d9a0f4 1607 initializeConnectorsMapStatus(this.evses.get(evseId)!.connectors, this.logPrefix());
ae25f265 1608 }
52952bf8 1609 this.saveEvsesStatus();
ae25f265
JB
1610 } else {
1611 logger.warn(
1612 `${this.logPrefix()} Charging station information from template ${
04b1261c 1613 this.templateFile
5edd8ba0 1614 } with no evses configuration defined, cannot create evses`,
04b1261c 1615 );
2585c6e9
JB
1616 }
1617 }
513db108
JB
1618 } else {
1619 logger.warn(
1620 `${this.logPrefix()} Charging station information from template ${
1621 this.templateFile
5edd8ba0 1622 } with no evses configuration defined, using already defined evses`,
513db108 1623 );
2585c6e9
JB
1624 }
1625 }
1626
551e477c
JB
1627 private getConfigurationFromFile(): ChargingStationConfiguration | undefined {
1628 let configuration: ChargingStationConfiguration | undefined;
9bf0ef23 1629 if (isNotEmptyString(this.configurationFile) && existsSync(this.configurationFile)) {
073bd098 1630 try {
57adbebc
JB
1631 if (this.sharedLRUCache.hasChargingStationConfiguration(this.configurationFileHash)) {
1632 configuration = this.sharedLRUCache.getChargingStationConfiguration(
5edd8ba0 1633 this.configurationFileHash,
57adbebc 1634 );
7c72977b
JB
1635 } else {
1636 const measureId = `${FileType.ChargingStationConfiguration} read`;
1637 const beginId = PerformanceStatistics.beginMeasure(measureId);
1638 configuration = JSON.parse(
5edd8ba0 1639 readFileSync(this.configurationFile, 'utf8'),
7c72977b
JB
1640 ) as ChargingStationConfiguration;
1641 PerformanceStatistics.endMeasure(measureId, beginId);
57adbebc 1642 this.sharedLRUCache.setChargingStationConfiguration(configuration);
e1d9a0f4 1643 this.configurationFileHash = configuration.configurationHash!;
7c72977b 1644 }
073bd098 1645 } catch (error) {
fa5995d6 1646 handleFileException(
073bd098 1647 this.configurationFile,
7164966d
JB
1648 FileType.ChargingStationConfiguration,
1649 error as NodeJS.ErrnoException,
5edd8ba0 1650 this.logPrefix(),
073bd098
JB
1651 );
1652 }
1653 }
1654 return configuration;
1655 }
1656
cb60061f 1657 private saveAutomaticTransactionGeneratorConfiguration(): void {
5ced7e80
JB
1658 if (this.getAutomaticTransactionGeneratorPersistentConfiguration()) {
1659 this.saveConfiguration();
1660 }
ac7f79af
JB
1661 }
1662
52952bf8 1663 private saveConnectorsStatus() {
7446de3b 1664 this.saveConfiguration();
52952bf8
JB
1665 }
1666
1667 private saveEvsesStatus() {
7446de3b 1668 this.saveConfiguration();
52952bf8
JB
1669 }
1670
179ed367 1671 private saveConfiguration(): void {
9bf0ef23 1672 if (isNotEmptyString(this.configurationFile)) {
2484ac1e 1673 try {
d972af76
JB
1674 if (!existsSync(dirname(this.configurationFile))) {
1675 mkdirSync(dirname(this.configurationFile), { recursive: true });
073bd098 1676 }
ae8ceef3
JB
1677 let configurationData: ChargingStationConfiguration = this.getConfigurationFromFile()
1678 ? cloneObject<ChargingStationConfiguration>(this.getConfigurationFromFile()!)
1679 : {};
34eeb1fb 1680 if (this.getStationInfoPersistentConfiguration() && this.stationInfo) {
52952bf8 1681 configurationData.stationInfo = this.stationInfo;
5ced7e80
JB
1682 } else {
1683 delete configurationData.stationInfo;
52952bf8 1684 }
34eeb1fb 1685 if (this.getOcppPersistentConfiguration() && this.ocppConfiguration?.configurationKey) {
52952bf8 1686 configurationData.configurationKey = this.ocppConfiguration.configurationKey;
5ced7e80
JB
1687 } else {
1688 delete configurationData.configurationKey;
52952bf8 1689 }
179ed367
JB
1690 configurationData = merge<ChargingStationConfiguration>(
1691 configurationData,
5edd8ba0 1692 buildChargingStationAutomaticTransactionGeneratorConfiguration(this),
179ed367 1693 );
5ced7e80
JB
1694 if (
1695 !this.getAutomaticTransactionGeneratorPersistentConfiguration() ||
1696 !this.getAutomaticTransactionGeneratorConfiguration()
1697 ) {
1698 delete configurationData.automaticTransactionGenerator;
1699 }
b1bbdae5 1700 if (this.connectors.size > 0) {
179ed367 1701 configurationData.connectorsStatus = buildConnectorsStatus(this);
5ced7e80
JB
1702 } else {
1703 delete configurationData.connectorsStatus;
52952bf8 1704 }
b1bbdae5 1705 if (this.evses.size > 0) {
179ed367 1706 configurationData.evsesStatus = buildEvsesStatus(this);
5ced7e80
JB
1707 } else {
1708 delete configurationData.evsesStatus;
52952bf8 1709 }
7c72977b 1710 delete configurationData.configurationHash;
d972af76 1711 const configurationHash = createHash(Constants.DEFAULT_HASH_ALGORITHM)
5ced7e80
JB
1712 .update(
1713 JSON.stringify({
1714 stationInfo: configurationData.stationInfo,
1715 configurationKey: configurationData.configurationKey,
1716 automaticTransactionGenerator: configurationData.automaticTransactionGenerator,
8ab96efb
JB
1717 ...(this.connectors.size > 0 && {
1718 connectorsStatus: configurationData.connectorsStatus,
1719 }),
1720 ...(this.evses.size > 0 && { evsesStatus: configurationData.evsesStatus }),
5edd8ba0 1721 } as ChargingStationConfiguration),
5ced7e80 1722 )
7c72977b
JB
1723 .digest('hex');
1724 if (this.configurationFileHash !== configurationHash) {
0ebf7c2e
JB
1725 AsyncLock.runExclusive(AsyncLockType.configuration, () => {
1726 configurationData.configurationHash = configurationHash;
1727 const measureId = `${FileType.ChargingStationConfiguration} write`;
1728 const beginId = PerformanceStatistics.beginMeasure(measureId);
1729 writeFileSync(
1730 this.configurationFile,
4ed03b6e 1731 JSON.stringify(configurationData, undefined, 2),
0ebf7c2e
JB
1732 'utf8',
1733 );
1734 PerformanceStatistics.endMeasure(measureId, beginId);
1735 this.sharedLRUCache.deleteChargingStationConfiguration(this.configurationFileHash);
1736 this.sharedLRUCache.setChargingStationConfiguration(configurationData);
1737 this.configurationFileHash = configurationHash;
1738 }).catch((error) => {
1739 handleFileException(
1740 this.configurationFile,
1741 FileType.ChargingStationConfiguration,
1742 error as NodeJS.ErrnoException,
1743 this.logPrefix(),
1744 );
1745 });
7c72977b
JB
1746 } else {
1747 logger.debug(
1748 `${this.logPrefix()} Not saving unchanged charging station configuration file ${
1749 this.configurationFile
5edd8ba0 1750 }`,
7c72977b 1751 );
2484ac1e 1752 }
2484ac1e 1753 } catch (error) {
fa5995d6 1754 handleFileException(
2484ac1e 1755 this.configurationFile,
7164966d
JB
1756 FileType.ChargingStationConfiguration,
1757 error as NodeJS.ErrnoException,
5edd8ba0 1758 this.logPrefix(),
073bd098
JB
1759 );
1760 }
2484ac1e
JB
1761 } else {
1762 logger.error(
5edd8ba0 1763 `${this.logPrefix()} Trying to save charging station configuration to undefined configuration file`,
2484ac1e 1764 );
073bd098
JB
1765 }
1766 }
1767
551e477c
JB
1768 private getOcppConfigurationFromTemplate(): ChargingStationOcppConfiguration | undefined {
1769 return this.getTemplateFromFile()?.Configuration;
2484ac1e
JB
1770 }
1771
551e477c 1772 private getOcppConfigurationFromFile(): ChargingStationOcppConfiguration | undefined {
60655b26
JB
1773 const configurationKey = this.getConfigurationFromFile()?.configurationKey;
1774 if (this.getOcppPersistentConfiguration() === true && configurationKey) {
1775 return { configurationKey };
648512ce 1776 }
60655b26 1777 return undefined;
7dde0b73
JB
1778 }
1779
551e477c
JB
1780 private getOcppConfiguration(): ChargingStationOcppConfiguration | undefined {
1781 let ocppConfiguration: ChargingStationOcppConfiguration | undefined =
72092cfc 1782 this.getOcppConfigurationFromFile();
2484ac1e
JB
1783 if (!ocppConfiguration) {
1784 ocppConfiguration = this.getOcppConfigurationFromTemplate();
1785 }
1786 return ocppConfiguration;
1787 }
1788
c0560973 1789 private async onOpen(): Promise<void> {
56eb297e 1790 if (this.isWebSocketConnectionOpened() === true) {
5144f4d1 1791 logger.info(
5edd8ba0 1792 `${this.logPrefix()} Connection to OCPP server through ${this.wsConnectionUrl.toString()} succeeded`,
5144f4d1 1793 );
ed6cfcff 1794 if (this.isRegistered() === false) {
5144f4d1
JB
1795 // Send BootNotification
1796 let registrationRetryCount = 0;
1797 do {
f7f98c68 1798 this.bootNotificationResponse = await this.ocppRequestService.requestHandler<
5144f4d1
JB
1799 BootNotificationRequest,
1800 BootNotificationResponse
8bfbc743
JB
1801 >(this, RequestCommand.BOOT_NOTIFICATION, this.bootNotificationRequest, {
1802 skipBufferingOnError: true,
1803 });
ed6cfcff 1804 if (this.isRegistered() === false) {
1fe0632a 1805 this.getRegistrationMaxRetries() !== -1 && ++registrationRetryCount;
9bf0ef23 1806 await sleep(
1895299d 1807 this?.bootNotificationResponse?.interval
be4c6702 1808 ? secondsToMilliseconds(this.bootNotificationResponse.interval)
5edd8ba0 1809 : Constants.DEFAULT_BOOT_NOTIFICATION_INTERVAL,
5144f4d1
JB
1810 );
1811 }
1812 } while (
ed6cfcff 1813 this.isRegistered() === false &&
e1d9a0f4 1814 (registrationRetryCount <= this.getRegistrationMaxRetries()! ||
5144f4d1
JB
1815 this.getRegistrationMaxRetries() === -1)
1816 );
1817 }
ed6cfcff 1818 if (this.isRegistered() === true) {
db54d2e0 1819 this.emit(ChargingStationEvents.registered);
f7c2994d 1820 if (this.inAcceptedState() === true) {
db54d2e0 1821 this.emit(ChargingStationEvents.accepted);
94bb24d5 1822 await this.startMessageSequence();
c0560973 1823 }
5144f4d1
JB
1824 } else {
1825 logger.error(
5edd8ba0 1826 `${this.logPrefix()} Registration failure: max retries reached (${this.getRegistrationMaxRetries()}) or retry disabled (${this.getRegistrationMaxRetries()})`,
5144f4d1 1827 );
caad9d6b 1828 }
5144f4d1 1829 this.wsConnectionRestarted = false;
aa428a31 1830 this.autoReconnectRetryCount = 0;
db54d2e0 1831 this.emit(ChargingStationEvents.updated);
2e6f5966 1832 } else {
5144f4d1 1833 logger.warn(
5edd8ba0 1834 `${this.logPrefix()} Connection to OCPP server through ${this.wsConnectionUrl.toString()} failed`,
e7aeea18 1835 );
2e6f5966 1836 }
2e6f5966
JB
1837 }
1838
ef7d8c21 1839 private async onClose(code: number, reason: Buffer): Promise<void> {
d09085e9 1840 switch (code) {
6c65a295
JB
1841 // Normal close
1842 case WebSocketCloseEventStatusCode.CLOSE_NORMAL:
c0560973 1843 case WebSocketCloseEventStatusCode.CLOSE_NO_STATUS:
e7aeea18 1844 logger.info(
9bf0ef23 1845 `${this.logPrefix()} WebSocket normally closed with status '${getWebSocketCloseEventStatusString(
5edd8ba0
JB
1846 code,
1847 )}' and reason '${reason.toString()}'`,
e7aeea18 1848 );
c0560973
JB
1849 this.autoReconnectRetryCount = 0;
1850 break;
6c65a295
JB
1851 // Abnormal close
1852 default:
e7aeea18 1853 logger.error(
9bf0ef23 1854 `${this.logPrefix()} WebSocket abnormally closed with status '${getWebSocketCloseEventStatusString(
5edd8ba0
JB
1855 code,
1856 )}' and reason '${reason.toString()}'`,
e7aeea18 1857 );
56eb297e 1858 this.started === true && (await this.reconnect());
c0560973
JB
1859 break;
1860 }
db54d2e0 1861 this.emit(ChargingStationEvents.updated);
2e6f5966
JB
1862 }
1863
56d09fd7
JB
1864 private getCachedRequest(messageType: MessageType, messageId: string): CachedRequest | undefined {
1865 const cachedRequest = this.requests.get(messageId);
1866 if (Array.isArray(cachedRequest) === true) {
1867 return cachedRequest;
1868 }
1869 throw new OCPPError(
1870 ErrorType.PROTOCOL_ERROR,
1871 `Cached request for message id ${messageId} ${OCPPServiceUtils.getMessageTypeString(
5edd8ba0 1872 messageType,
56d09fd7
JB
1873 )} is not an array`,
1874 undefined,
5edd8ba0 1875 cachedRequest as JsonType,
56d09fd7
JB
1876 );
1877 }
1878
1879 private async handleIncomingMessage(request: IncomingRequest): Promise<void> {
1880 const [messageType, messageId, commandName, commandPayload] = request;
1881 if (this.getEnableStatistics() === true) {
1882 this.performanceStatistics?.addRequestStatistic(commandName, messageType);
1883 }
1884 logger.debug(
1885 `${this.logPrefix()} << Command '${commandName}' received request payload: ${JSON.stringify(
5edd8ba0
JB
1886 request,
1887 )}`,
56d09fd7
JB
1888 );
1889 // Process the message
1890 await this.ocppIncomingRequestService.incomingRequestHandler(
1891 this,
1892 messageId,
1893 commandName,
5edd8ba0 1894 commandPayload,
56d09fd7
JB
1895 );
1896 }
1897
1898 private handleResponseMessage(response: Response): void {
1899 const [messageType, messageId, commandPayload] = response;
1900 if (this.requests.has(messageId) === false) {
1901 // Error
1902 throw new OCPPError(
1903 ErrorType.INTERNAL_ERROR,
1904 `Response for unknown message id ${messageId}`,
1905 undefined,
5edd8ba0 1906 commandPayload,
56d09fd7
JB
1907 );
1908 }
1909 // Respond
1910 const [responseCallback, , requestCommandName, requestPayload] = this.getCachedRequest(
1911 messageType,
5edd8ba0 1912 messageId,
e1d9a0f4 1913 )!;
56d09fd7
JB
1914 logger.debug(
1915 `${this.logPrefix()} << Command '${
1916 requestCommandName ?? Constants.UNKNOWN_COMMAND
5edd8ba0 1917 }' received response payload: ${JSON.stringify(response)}`,
56d09fd7
JB
1918 );
1919 responseCallback(commandPayload, requestPayload);
1920 }
1921
1922 private handleErrorMessage(errorResponse: ErrorResponse): void {
1923 const [messageType, messageId, errorType, errorMessage, errorDetails] = errorResponse;
1924 if (this.requests.has(messageId) === false) {
1925 // Error
1926 throw new OCPPError(
1927 ErrorType.INTERNAL_ERROR,
1928 `Error response for unknown message id ${messageId}`,
1929 undefined,
5edd8ba0 1930 { errorType, errorMessage, errorDetails },
56d09fd7
JB
1931 );
1932 }
e1d9a0f4 1933 const [, errorCallback, requestCommandName] = this.getCachedRequest(messageType, messageId)!;
56d09fd7
JB
1934 logger.debug(
1935 `${this.logPrefix()} << Command '${
1936 requestCommandName ?? Constants.UNKNOWN_COMMAND
5edd8ba0 1937 }' received error response payload: ${JSON.stringify(errorResponse)}`,
56d09fd7
JB
1938 );
1939 errorCallback(new OCPPError(errorType, errorMessage, requestCommandName, errorDetails));
1940 }
1941
ef7d8c21 1942 private async onMessage(data: RawData): Promise<void> {
e1d9a0f4
JB
1943 let request: IncomingRequest | Response | ErrorResponse | undefined;
1944 let messageType: number | undefined;
ded57f02 1945 let errorMsg: string;
c0560973 1946 try {
e1d9a0f4 1947 // eslint-disable-next-line @typescript-eslint/no-base-to-string
56d09fd7 1948 request = JSON.parse(data.toString()) as IncomingRequest | Response | ErrorResponse;
53e5fd67 1949 if (Array.isArray(request) === true) {
56d09fd7 1950 [messageType] = request;
b3ec7bc1
JB
1951 // Check the type of message
1952 switch (messageType) {
1953 // Incoming Message
1954 case MessageType.CALL_MESSAGE:
56d09fd7 1955 await this.handleIncomingMessage(request as IncomingRequest);
b3ec7bc1 1956 break;
56d09fd7 1957 // Response Message
b3ec7bc1 1958 case MessageType.CALL_RESULT_MESSAGE:
56d09fd7 1959 this.handleResponseMessage(request as Response);
a2d1c0f1
JB
1960 break;
1961 // Error Message
1962 case MessageType.CALL_ERROR_MESSAGE:
56d09fd7 1963 this.handleErrorMessage(request as ErrorResponse);
b3ec7bc1 1964 break;
56d09fd7 1965 // Unknown Message
b3ec7bc1
JB
1966 default:
1967 // eslint-disable-next-line @typescript-eslint/restrict-template-expressions
ded57f02
JB
1968 errorMsg = `Wrong message type ${messageType}`;
1969 logger.error(`${this.logPrefix()} ${errorMsg}`);
1970 throw new OCPPError(ErrorType.PROTOCOL_ERROR, errorMsg);
b3ec7bc1 1971 }
db54d2e0 1972 this.emit(ChargingStationEvents.updated);
47e22477 1973 } else {
e1d9a0f4
JB
1974 throw new OCPPError(
1975 ErrorType.PROTOCOL_ERROR,
1976 'Incoming message is not an array',
1977 undefined,
1978 {
1979 request,
1980 },
1981 );
47e22477 1982 }
c0560973 1983 } catch (error) {
e1d9a0f4
JB
1984 let commandName: IncomingRequestCommand | undefined;
1985 let requestCommandName: RequestCommand | IncomingRequestCommand | undefined;
56d09fd7 1986 let errorCallback: ErrorCallback;
e1d9a0f4 1987 const [, messageId] = request!;
13701f69
JB
1988 switch (messageType) {
1989 case MessageType.CALL_MESSAGE:
56d09fd7 1990 [, , commandName] = request as IncomingRequest;
13701f69 1991 // Send error
56d09fd7 1992 await this.ocppRequestService.sendError(this, messageId, error as OCPPError, commandName);
13701f69
JB
1993 break;
1994 case MessageType.CALL_RESULT_MESSAGE:
1995 case MessageType.CALL_ERROR_MESSAGE:
56d09fd7 1996 if (this.requests.has(messageId) === true) {
e1d9a0f4 1997 [, errorCallback, requestCommandName] = this.getCachedRequest(messageType, messageId)!;
13701f69
JB
1998 // Reject the deferred promise in case of error at response handling (rejecting an already fulfilled promise is a no-op)
1999 errorCallback(error as OCPPError, false);
2000 } else {
2001 // Remove the request from the cache in case of error at response handling
2002 this.requests.delete(messageId);
2003 }
de4cb8b6 2004 break;
ba7965c4 2005 }
56d09fd7
JB
2006 if (error instanceof OCPPError === false) {
2007 logger.warn(
2008 `${this.logPrefix()} Error thrown at incoming OCPP command '${
2009 commandName ?? requestCommandName ?? Constants.UNKNOWN_COMMAND
e1d9a0f4 2010 // eslint-disable-next-line @typescript-eslint/no-base-to-string
56d09fd7 2011 }' message '${data.toString()}' handling is not an OCPPError:`,
5edd8ba0 2012 error,
56d09fd7
JB
2013 );
2014 }
2015 logger.error(
2016 `${this.logPrefix()} Incoming OCPP command '${
2017 commandName ?? requestCommandName ?? Constants.UNKNOWN_COMMAND
e1d9a0f4 2018 // eslint-disable-next-line @typescript-eslint/no-base-to-string
56d09fd7
JB
2019 }' message '${data.toString()}'${
2020 messageType !== MessageType.CALL_MESSAGE
2021 ? ` matching cached request '${JSON.stringify(this.requests.get(messageId))}'`
2022 : ''
2023 } processing error:`,
5edd8ba0 2024 error,
56d09fd7 2025 );
c0560973 2026 }
2328be1e
JB
2027 }
2028
c0560973 2029 private onPing(): void {
44eb6026 2030 logger.debug(`${this.logPrefix()} Received a WS ping (rfc6455) from the server`);
c0560973
JB
2031 }
2032
2033 private onPong(): void {
44eb6026 2034 logger.debug(`${this.logPrefix()} Received a WS pong (rfc6455) from the server`);
c0560973
JB
2035 }
2036
9534e74e 2037 private onError(error: WSError): void {
bcc9c3c0 2038 this.closeWSConnection();
44eb6026 2039 logger.error(`${this.logPrefix()} WebSocket error:`, error);
c0560973
JB
2040 }
2041
18bf8274 2042 private getEnergyActiveImportRegister(connectorStatus: ConnectorStatus, rounded = false): number {
95bdbf12 2043 if (this.getMeteringPerTransaction() === true) {
07989fad 2044 return (
18bf8274 2045 (rounded === true
e1d9a0f4 2046 ? Math.round(connectorStatus.transactionEnergyActiveImportRegisterValue!)
07989fad
JB
2047 : connectorStatus?.transactionEnergyActiveImportRegisterValue) ?? 0
2048 );
2049 }
2050 return (
18bf8274 2051 (rounded === true
e1d9a0f4 2052 ? Math.round(connectorStatus.energyActiveImportRegisterValue!)
07989fad
JB
2053 : connectorStatus?.energyActiveImportRegisterValue) ?? 0
2054 );
2055 }
2056
cda5d0fb
JB
2057 private getUseConnectorId0(stationTemplate?: ChargingStationTemplate): boolean {
2058 return stationTemplate?.useConnectorId0 ?? true;
8bce55bf
JB
2059 }
2060
9ff486f4 2061 private async stopRunningTransactions(reason?: StopTransactionReason): Promise<void> {
28e78158 2062 if (this.hasEvses) {
3fa7f799
JB
2063 for (const [evseId, evseStatus] of this.evses) {
2064 if (evseId === 0) {
2065 continue;
2066 }
28e78158
JB
2067 for (const [connectorId, connectorStatus] of evseStatus.connectors) {
2068 if (connectorStatus.transactionStarted === true) {
2069 await this.stopTransactionOnConnector(connectorId, reason);
2070 }
2071 }
2072 }
2073 } else {
2074 for (const connectorId of this.connectors.keys()) {
2075 if (connectorId > 0 && this.getConnectorStatus(connectorId)?.transactionStarted === true) {
2076 await this.stopTransactionOnConnector(connectorId, reason);
2077 }
60ddad53
JB
2078 }
2079 }
2080 }
2081
1f761b9a 2082 // 0 for disabling
c72f6634 2083 private getConnectionTimeout(): number {
f2d5e3d9 2084 if (getConfigurationKey(this, StandardParametersKey.ConnectionTimeOut)) {
e7aeea18 2085 return (
f2d5e3d9
JB
2086 parseInt(getConfigurationKey(this, StandardParametersKey.ConnectionTimeOut)!.value!) ??
2087 Constants.DEFAULT_CONNECTION_TIMEOUT
e7aeea18 2088 );
291cb255 2089 }
291cb255 2090 return Constants.DEFAULT_CONNECTION_TIMEOUT;
3574dfd3
JB
2091 }
2092
1f761b9a 2093 // -1 for unlimited, 0 for disabling
72092cfc 2094 private getAutoReconnectMaxRetries(): number | undefined {
29b34879 2095 return this.stationInfo.autoReconnectMaxRetries ?? -1;
3574dfd3
JB
2096 }
2097
b89fb74f 2098 // -1 for unlimited, 0 for disabling
72092cfc 2099 private getRegistrationMaxRetries(): number | undefined {
b1bbdae5 2100 return this.stationInfo.registrationMaxRetries ?? -1;
32a1eb7a
JB
2101 }
2102
c0560973 2103 private getPowerDivider(): number {
b1bbdae5 2104 let powerDivider = this.hasEvses ? this.getNumberOfEvses() : this.getNumberOfConnectors();
fa7bccf4 2105 if (this.stationInfo?.powerSharedByConnectors) {
c0560973 2106 powerDivider = this.getNumberOfRunningTransactions();
6ecb15e4
JB
2107 }
2108 return powerDivider;
2109 }
2110
fa7bccf4
JB
2111 private getMaximumAmperage(stationInfo: ChargingStationInfo): number | undefined {
2112 const maximumPower = this.getMaximumPower(stationInfo);
2113 switch (this.getCurrentOutType(stationInfo)) {
cc6e8ab5
JB
2114 case CurrentType.AC:
2115 return ACElectricUtils.amperagePerPhaseFromPower(
fa7bccf4 2116 this.getNumberOfPhases(stationInfo),
b1bbdae5 2117 maximumPower / (this.hasEvses ? this.getNumberOfEvses() : this.getNumberOfConnectors()),
5edd8ba0 2118 this.getVoltageOut(stationInfo),
cc6e8ab5
JB
2119 );
2120 case CurrentType.DC:
fa7bccf4 2121 return DCElectricUtils.amperage(maximumPower, this.getVoltageOut(stationInfo));
cc6e8ab5
JB
2122 }
2123 }
2124
cc6e8ab5
JB
2125 private getAmperageLimitation(): number | undefined {
2126 if (
9bf0ef23 2127 isNotEmptyString(this.stationInfo?.amperageLimitationOcppKey) &&
f2d5e3d9 2128 getConfigurationKey(this, this.stationInfo.amperageLimitationOcppKey!)
cc6e8ab5
JB
2129 ) {
2130 return (
9bf0ef23 2131 convertToInt(
f2d5e3d9 2132 getConfigurationKey(this, this.stationInfo.amperageLimitationOcppKey!)?.value,
fba11dc6 2133 ) / getAmperageLimitationUnitDivider(this.stationInfo)
cc6e8ab5
JB
2134 );
2135 }
2136 }
2137
c0560973 2138 private async startMessageSequence(): Promise<void> {
b7f9e41d 2139 if (this.stationInfo?.autoRegister === true) {
f7f98c68 2140 await this.ocppRequestService.requestHandler<
ef6fa3fb
JB
2141 BootNotificationRequest,
2142 BootNotificationResponse
8bfbc743
JB
2143 >(this, RequestCommand.BOOT_NOTIFICATION, this.bootNotificationRequest, {
2144 skipBufferingOnError: true,
2145 });
6114e6f1 2146 }
136c90ba 2147 // Start WebSocket ping
c0560973 2148 this.startWebSocketPing();
5ad8570f 2149 // Start heartbeat
c0560973 2150 this.startHeartbeat();
0a60c33c 2151 // Initialize connectors status
c3b83130
JB
2152 if (this.hasEvses) {
2153 for (const [evseId, evseStatus] of this.evses) {
4334db72
JB
2154 if (evseId > 0) {
2155 for (const [connectorId, connectorStatus] of evseStatus.connectors) {
fba11dc6 2156 const connectorBootStatus = getBootConnectorStatus(this, connectorId, connectorStatus);
4334db72
JB
2157 await OCPPServiceUtils.sendAndSetConnectorStatus(
2158 this,
2159 connectorId,
12f26d4a 2160 connectorBootStatus,
5edd8ba0 2161 evseId,
4334db72
JB
2162 );
2163 }
c3b83130 2164 }
4334db72
JB
2165 }
2166 } else {
2167 for (const connectorId of this.connectors.keys()) {
2168 if (connectorId > 0) {
fba11dc6 2169 const connectorBootStatus = getBootConnectorStatus(
c3b83130
JB
2170 this,
2171 connectorId,
e1d9a0f4 2172 this.getConnectorStatus(connectorId)!,
c3b83130
JB
2173 );
2174 await OCPPServiceUtils.sendAndSetConnectorStatus(this, connectorId, connectorBootStatus);
2175 }
2176 }
5ad8570f 2177 }
c9a4f9ea
JB
2178 if (this.stationInfo?.firmwareStatus === FirmwareStatus.Installing) {
2179 await this.ocppRequestService.requestHandler<
2180 FirmwareStatusNotificationRequest,
2181 FirmwareStatusNotificationResponse
2182 >(this, RequestCommand.FIRMWARE_STATUS_NOTIFICATION, {
2183 status: FirmwareStatus.Installed,
2184 });
2185 this.stationInfo.firmwareStatus = FirmwareStatus.Installed;
c9a4f9ea 2186 }
3637ca2c 2187
0a60c33c 2188 // Start the ATG
ac7f79af 2189 if (this.getAutomaticTransactionGeneratorConfiguration()?.enable === true) {
4f69be04 2190 this.startAutomaticTransactionGenerator();
fa7bccf4 2191 }
aa428a31 2192 this.wsConnectionRestarted === true && this.flushMessageBuffer();
fa7bccf4
JB
2193 }
2194
e7aeea18 2195 private async stopMessageSequence(
9ff486f4
JB
2196 reason?: StopTransactionReason,
2197 stopTransactions = true,
e7aeea18 2198 ): Promise<void> {
136c90ba 2199 // Stop WebSocket ping
c0560973 2200 this.stopWebSocketPing();
79411696 2201 // Stop heartbeat
c0560973 2202 this.stopHeartbeat();
fa7bccf4 2203 // Stop ongoing transactions
9ff486f4
JB
2204 stopTransactions && (await this.stopRunningTransactions(reason));
2205 // Stop the ATG
b20eb107 2206 if (this.automaticTransactionGenerator?.started === true) {
9ff486f4 2207 this.stopAutomaticTransactionGenerator();
79411696 2208 }
039211f9
JB
2209 if (this.hasEvses) {
2210 for (const [evseId, evseStatus] of this.evses) {
2211 if (evseId > 0) {
2212 for (const [connectorId, connectorStatus] of evseStatus.connectors) {
2213 await this.ocppRequestService.requestHandler<
2214 StatusNotificationRequest,
2215 StatusNotificationResponse
2216 >(
2217 this,
2218 RequestCommand.STATUS_NOTIFICATION,
2219 OCPPServiceUtils.buildStatusNotificationRequest(
2220 this,
2221 connectorId,
12f26d4a 2222 ConnectorStatusEnum.Unavailable,
5edd8ba0
JB
2223 evseId,
2224 ),
039211f9
JB
2225 );
2226 delete connectorStatus?.status;
2227 }
2228 }
2229 }
2230 } else {
2231 for (const connectorId of this.connectors.keys()) {
2232 if (connectorId > 0) {
2233 await this.ocppRequestService.requestHandler<
2234 StatusNotificationRequest,
2235 StatusNotificationResponse
2236 >(
6e939d9e 2237 this,
039211f9
JB
2238 RequestCommand.STATUS_NOTIFICATION,
2239 OCPPServiceUtils.buildStatusNotificationRequest(
2240 this,
2241 connectorId,
5edd8ba0
JB
2242 ConnectorStatusEnum.Unavailable,
2243 ),
039211f9
JB
2244 );
2245 delete this.getConnectorStatus(connectorId)?.status;
2246 }
45c0ae82
JB
2247 }
2248 }
79411696
JB
2249 }
2250
c0560973 2251 private startWebSocketPing(): void {
f2d5e3d9 2252 const webSocketPingInterval: number = getConfigurationKey(
17ac262c 2253 this,
5edd8ba0 2254 StandardParametersKey.WebSocketPingInterval,
e7aeea18 2255 )
f2d5e3d9 2256 ? convertToInt(getConfigurationKey(this, StandardParametersKey.WebSocketPingInterval)?.value)
9cd3dfb0 2257 : 0;
ad2f27c3
JB
2258 if (webSocketPingInterval > 0 && !this.webSocketPingSetInterval) {
2259 this.webSocketPingSetInterval = setInterval(() => {
56eb297e 2260 if (this.isWebSocketConnectionOpened() === true) {
72092cfc 2261 this.wsConnection?.ping();
136c90ba 2262 }
be4c6702 2263 }, secondsToMilliseconds(webSocketPingInterval));
e7aeea18 2264 logger.info(
9bf0ef23 2265 `${this.logPrefix()} WebSocket ping started every ${formatDurationSeconds(
5edd8ba0
JB
2266 webSocketPingInterval,
2267 )}`,
e7aeea18 2268 );
ad2f27c3 2269 } else if (this.webSocketPingSetInterval) {
e7aeea18 2270 logger.info(
9bf0ef23 2271 `${this.logPrefix()} WebSocket ping already started every ${formatDurationSeconds(
5edd8ba0
JB
2272 webSocketPingInterval,
2273 )}`,
e7aeea18 2274 );
136c90ba 2275 } else {
e7aeea18 2276 logger.error(
5edd8ba0 2277 `${this.logPrefix()} WebSocket ping interval set to ${webSocketPingInterval}, not starting the WebSocket ping`,
e7aeea18 2278 );
136c90ba
JB
2279 }
2280 }
2281
c0560973 2282 private stopWebSocketPing(): void {
ad2f27c3
JB
2283 if (this.webSocketPingSetInterval) {
2284 clearInterval(this.webSocketPingSetInterval);
dfe81c8f 2285 delete this.webSocketPingSetInterval;
136c90ba
JB
2286 }
2287 }
2288
1f5df42a 2289 private getConfiguredSupervisionUrl(): URL {
d5c3df49 2290 let configuredSupervisionUrl: string;
72092cfc 2291 const supervisionUrls = this.stationInfo?.supervisionUrls ?? Configuration.getSupervisionUrls();
9bf0ef23 2292 if (isNotEmptyArray(supervisionUrls)) {
269de583 2293 let configuredSupervisionUrlIndex: number;
2dcfe98e 2294 switch (Configuration.getSupervisionUrlDistribution()) {
2dcfe98e 2295 case SupervisionUrlDistribution.RANDOM:
e1d9a0f4
JB
2296 configuredSupervisionUrlIndex = Math.floor(
2297 secureRandom() * (supervisionUrls as string[]).length,
2298 );
2dcfe98e 2299 break;
a52a6446 2300 case SupervisionUrlDistribution.ROUND_ROBIN:
c72f6634 2301 case SupervisionUrlDistribution.CHARGING_STATION_AFFINITY:
2dcfe98e 2302 default:
a52a6446 2303 Object.values(SupervisionUrlDistribution).includes(
e1d9a0f4 2304 Configuration.getSupervisionUrlDistribution()!,
a52a6446
JB
2305 ) === false &&
2306 logger.error(
e1d9a0f4 2307 // eslint-disable-next-line @typescript-eslint/no-base-to-string
a52a6446
JB
2308 `${this.logPrefix()} Unknown supervision url distribution '${Configuration.getSupervisionUrlDistribution()}' from values '${SupervisionUrlDistribution.toString()}', defaulting to ${
2309 SupervisionUrlDistribution.CHARGING_STATION_AFFINITY
5edd8ba0 2310 }`,
a52a6446 2311 );
e1d9a0f4 2312 configuredSupervisionUrlIndex = (this.index - 1) % (supervisionUrls as string[]).length;
2dcfe98e 2313 break;
c0560973 2314 }
e1d9a0f4 2315 configuredSupervisionUrl = (supervisionUrls as string[])[configuredSupervisionUrlIndex];
d5c3df49
JB
2316 } else {
2317 configuredSupervisionUrl = supervisionUrls as string;
2318 }
9bf0ef23 2319 if (isNotEmptyString(configuredSupervisionUrl)) {
d5c3df49 2320 return new URL(configuredSupervisionUrl);
c0560973 2321 }
49c508b0 2322 const errorMsg = 'No supervision url(s) configured';
7f77d16f
JB
2323 logger.error(`${this.logPrefix()} ${errorMsg}`);
2324 throw new BaseError(`${errorMsg}`);
136c90ba
JB
2325 }
2326
c0560973 2327 private stopHeartbeat(): void {
ad2f27c3
JB
2328 if (this.heartbeatSetInterval) {
2329 clearInterval(this.heartbeatSetInterval);
dfe81c8f 2330 delete this.heartbeatSetInterval;
7dde0b73 2331 }
5ad8570f
JB
2332 }
2333
55516218 2334 private terminateWSConnection(): void {
56eb297e 2335 if (this.isWebSocketConnectionOpened() === true) {
72092cfc 2336 this.wsConnection?.terminate();
55516218
JB
2337 this.wsConnection = null;
2338 }
2339 }
2340
c72f6634 2341 private getReconnectExponentialDelay(): boolean {
a14885a3 2342 return this.stationInfo?.reconnectExponentialDelay ?? false;
5ad8570f
JB
2343 }
2344
aa428a31 2345 private async reconnect(): Promise<void> {
7874b0b1
JB
2346 // Stop WebSocket ping
2347 this.stopWebSocketPing();
136c90ba 2348 // Stop heartbeat
c0560973 2349 this.stopHeartbeat();
5ad8570f 2350 // Stop the ATG if needed
ac7f79af 2351 if (this.getAutomaticTransactionGeneratorConfiguration().stopOnConnectionFailure === true) {
9ff486f4 2352 this.stopAutomaticTransactionGenerator();
ad2f27c3 2353 }
e7aeea18 2354 if (
e1d9a0f4 2355 this.autoReconnectRetryCount < this.getAutoReconnectMaxRetries()! ||
e7aeea18
JB
2356 this.getAutoReconnectMaxRetries() === -1
2357 ) {
1fe0632a 2358 ++this.autoReconnectRetryCount;
e7aeea18 2359 const reconnectDelay = this.getReconnectExponentialDelay()
9bf0ef23 2360 ? exponentialDelay(this.autoReconnectRetryCount)
be4c6702 2361 : secondsToMilliseconds(this.getConnectionTimeout());
1e080116
JB
2362 const reconnectDelayWithdraw = 1000;
2363 const reconnectTimeout =
2364 reconnectDelay && reconnectDelay - reconnectDelayWithdraw > 0
2365 ? reconnectDelay - reconnectDelayWithdraw
2366 : 0;
e7aeea18 2367 logger.error(
9bf0ef23 2368 `${this.logPrefix()} WebSocket connection retry in ${roundTo(
e7aeea18 2369 reconnectDelay,
5edd8ba0
JB
2370 2,
2371 )}ms, timeout ${reconnectTimeout}ms`,
e7aeea18 2372 );
9bf0ef23 2373 await sleep(reconnectDelay);
e7aeea18 2374 logger.error(
5edd8ba0 2375 `${this.logPrefix()} WebSocket connection retry #${this.autoReconnectRetryCount.toString()}`,
e7aeea18
JB
2376 );
2377 this.openWSConnection(
59b6ed8d 2378 {
59b6ed8d
JB
2379 handshakeTimeout: reconnectTimeout,
2380 },
5edd8ba0 2381 { closeOpened: true },
e7aeea18 2382 );
265e4266 2383 this.wsConnectionRestarted = true;
c0560973 2384 } else if (this.getAutoReconnectMaxRetries() !== -1) {
e7aeea18 2385 logger.error(
d56ea27c 2386 `${this.logPrefix()} WebSocket connection retries failure: maximum retries reached (${
e7aeea18 2387 this.autoReconnectRetryCount
5edd8ba0 2388 }) or retries disabled (${this.getAutoReconnectMaxRetries()})`,
e7aeea18 2389 );
5ad8570f
JB
2390 }
2391 }
7dde0b73 2392}