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