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