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