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