refactor: rewriting functionalities and added additional helper functions
[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 {
834 const automaticTransactionGeneratorConfigurationFromFile =
835 this.getConfigurationFromFile()?.automaticTransactionGenerator;
836 if (automaticTransactionGeneratorConfigurationFromFile) {
837 return automaticTransactionGeneratorConfigurationFromFile;
838 }
839 return this.getTemplateFromFile()?.AutomaticTransactionGenerator;
840 }
841
842 public startAutomaticTransactionGenerator(connectorIds?: number[]): void {
843 this.automaticTransactionGenerator = AutomaticTransactionGenerator.getInstance(this);
53ac516c 844 if (Utils.isNotEmptyArray(connectorIds)) {
a5e9befc 845 for (const connectorId of connectorIds) {
551e477c 846 this.automaticTransactionGenerator?.startConnector(connectorId);
a5e9befc
JB
847 }
848 } else {
551e477c 849 this.automaticTransactionGenerator?.start();
4f69be04 850 }
ac7f79af 851 this.saveChargingStationAutomaticTransactionGeneratorConfiguration();
1895299d 852 parentPort?.postMessage(MessageChannelUtils.buildUpdatedMessage(this));
4f69be04
JB
853 }
854
a5e9befc 855 public stopAutomaticTransactionGenerator(connectorIds?: number[]): void {
53ac516c 856 if (Utils.isNotEmptyArray(connectorIds)) {
a5e9befc
JB
857 for (const connectorId of connectorIds) {
858 this.automaticTransactionGenerator?.stopConnector(connectorId);
859 }
860 } else {
861 this.automaticTransactionGenerator?.stop();
4f69be04 862 }
ac7f79af 863 this.saveChargingStationAutomaticTransactionGeneratorConfiguration();
1895299d 864 parentPort?.postMessage(MessageChannelUtils.buildUpdatedMessage(this));
4f69be04
JB
865 }
866
5e3cb728
JB
867 public async stopTransactionOnConnector(
868 connectorId: number,
869 reason = StopTransactionReason.NONE
870 ): Promise<StopTransactionResponse> {
72092cfc 871 const transactionId = this.getConnectorStatus(connectorId)?.transactionId;
5e3cb728 872 if (
c7e8e0a2
JB
873 this.getBeginEndMeterValues() === true &&
874 this.getOcppStrictCompliance() === true &&
875 this.getOutOfOrderEndMeterValues() === false
5e3cb728
JB
876 ) {
877 // FIXME: Implement OCPP version agnostic helpers
878 const transactionEndMeterValue = OCPP16ServiceUtils.buildTransactionEndMeterValue(
879 this,
880 connectorId,
881 this.getEnergyActiveImportRegisterByTransactionId(transactionId)
882 );
883 await this.ocppRequestService.requestHandler<MeterValuesRequest, MeterValuesResponse>(
884 this,
885 RequestCommand.METER_VALUES,
886 {
887 connectorId,
888 transactionId,
889 meterValue: [transactionEndMeterValue],
890 }
891 );
892 }
893 return this.ocppRequestService.requestHandler<StopTransactionRequest, StopTransactionResponse>(
894 this,
895 RequestCommand.STOP_TRANSACTION,
896 {
897 transactionId,
898 meterStop: this.getEnergyActiveImportRegisterByTransactionId(transactionId, true),
5e3cb728
JB
899 reason,
900 }
901 );
902 }
903
24578c31
JB
904 public supportsReservations(): boolean {
905 logger.info(`${this.logPrefix()} Check for reservation support in charging station`);
906 return ChargingStationConfigurationUtils.getConfigurationKey(
907 this,
908 StandardParametersKey.SupportedFeatureProfiles
909 ).value.includes(OCPP16SupportedFeatureProfiles.Reservation);
910 }
911
912 public supportsReservationsOnConnectorId0(): boolean {
913 logger.info(
d193a949 914 ` ${this.logPrefix()} Check for reservation support on connector 0 in charging station (CS)`
24578c31
JB
915 );
916 return (
917 this.supportsReservations() &&
918 ChargingStationConfigurationUtils.getConfigurationKey(
919 this,
920 OCPPConstants.OCPP_RESERVE_CONNECTOR_ZERO_SUPPORTED
921 ).value === 'true'
922 );
923 }
924
d193a949 925 public async addReservation(reservation: Reservation): Promise<void> {
24578c31
JB
926 if (Utils.isNullOrUndefined(this.reservations)) {
927 this.reservations = [];
928 }
d193a949 929 const [exists, reservationFound] = this.doesReservationExists(reservation);
24578c31 930 if (exists) {
d193a949
JB
931 await this.removeReservation(reservationFound);
932 }
933 this.reservations.push(reservation);
934 if (reservation.connectorId === 0) {
935 return;
24578c31 936 }
d193a949
JB
937 this.getConnectorStatus(reservation.connectorId).status = ConnectorStatusEnum.Reserved;
938 await this.ocppRequestService.requestHandler<
939 StatusNotificationRequest,
940 StatusNotificationResponse
941 >(
942 this,
943 RequestCommand.STATUS_NOTIFICATION,
944 OCPPServiceUtils.buildStatusNotificationRequest(
945 this,
946 reservation.connectorId,
947 ConnectorStatusEnum.Reserved
948 )
949 );
24578c31
JB
950 }
951
d193a949
JB
952 public async removeReservation(
953 reservation: Reservation,
954 reason?: ReservationTerminationReason
955 ): Promise<void> {
956 const sameReservation = (r: Reservation) => r.id === reservation.id;
957 const index = this.reservations?.findIndex(sameReservation);
24578c31 958 this.reservations.splice(index, 1);
d193a949
JB
959 switch (reason) {
960 case ReservationTerminationReason.TRANSACTION_STARTED:
961 // No action needed
962 break;
963 case ReservationTerminationReason.CONNECTOR_STATE_CHANGED:
964 // No action needed
965 break;
966 default: // ReservationTerminationReason.EXPIRED, ReservationTerminationReason.CANCELED
967 this.getConnectorStatus(reservation.connectorId).status = ConnectorStatusEnum.Available;
968 await this.ocppRequestService.requestHandler<
969 StatusNotificationRequest,
970 StatusNotificationResponse
971 >(
972 this,
973 RequestCommand.STATUS_NOTIFICATION,
974 OCPPServiceUtils.buildStatusNotificationRequest(
975 this,
976 reservation.connectorId,
977 ConnectorStatusEnum.Available
978 )
979 );
980 break;
981 }
24578c31
JB
982 }
983
d193a949
JB
984 public getReservationById(id: number): Reservation {
985 return this.reservations?.find((reservation) => reservation.id === id);
24578c31
JB
986 }
987
d193a949
JB
988 public getReservationByIdTag(id: string): Reservation {
989 return this.reservations?.find((reservation) => reservation.idTag === id);
990 }
991
992 public getReservationByConnectorId(id: number): Reservation {
993 return this.reservations?.find((reservation) => reservation.connectorId === id);
994 }
995
996 public doesReservationExists(reservation: Partial<Reservation>): [boolean, Reservation] {
997 const sameReservation = (r: Reservation) => r.id === reservation.id;
998 const foundReservation = this.reservations?.find(sameReservation);
24578c31
JB
999 return Utils.isUndefined(foundReservation) ? [false, null] : [true, foundReservation];
1000 }
1001
d193a949
JB
1002 public async isAuthorized(
1003 connectorId: number,
1004 idTag: string,
1005 parentIdTag?: string
1006 ): Promise<boolean> {
1007 let authorized = false;
1008 const connectorStatus = this.getConnectorStatus(connectorId);
1009 if (
1010 this.getLocalAuthListEnabled() === true &&
1011 this.hasIdTags() === true &&
1012 Utils.isNotEmptyString(
1013 this.idTagsCache
1014 .getIdTags(ChargingStationUtils.getIdTagsFile(this.stationInfo))
1015 ?.find((tag) => tag === idTag)
1016 )
1017 ) {
1018 connectorStatus.localAuthorizeIdTag = idTag;
1019 connectorStatus.idTagLocalAuthorized = true;
1020 authorized = true;
1021 } else if (this.getMustAuthorizeAtRemoteStart() === true) {
1022 connectorStatus.authorizeIdTag = idTag;
1023 const authorizeResponse: OCPP16AuthorizeResponse =
1024 await this.ocppRequestService.requestHandler<
1025 OCPP16AuthorizeRequest,
1026 OCPP16AuthorizeResponse
1027 >(this, OCPP16RequestCommand.AUTHORIZE, {
1028 idTag: idTag,
1029 });
1030 if (authorizeResponse?.idTagInfo?.status === OCPP16AuthorizationStatus.ACCEPTED) {
1031 authorized = true;
1032 }
1033 } else {
1034 logger.warn(
1035 `${this.logPrefix()} The charging station configuration expects authorize at
1036 remote start transaction but local authorization or authorize isn't enabled`
1037 );
1038 }
1039 return authorized;
24578c31
JB
1040 }
1041
d193a949
JB
1042 public startReservationExpiryDateSetInterval(customInterval?: number): void {
1043 const interval =
1044 customInterval ?? Constants.DEFAULT_RESERVATION_EXPIRATION_OBSERVATION_INTERVAL;
1045 logger.info(
1046 `${this.logPrefix()} Reservation expiration date interval is set to ${interval}
1047 and starts on CS now`
1048 );
1049 // eslint-disable-next-line @typescript-eslint/no-misused-promises
1050 this.reservationExpiryDateSetInterval = setInterval(async (): Promise<void> => {
1051 if (!Utils.isNullOrUndefined(this.reservations) && !Utils.isEmptyArray(this.reservations)) {
1052 for (const reservation of this.reservations) {
1053 if (reservation.expiryDate.toString() < new Date().toISOString()) {
1054 await this.removeReservation(reservation);
1055 logger.info(
1056 `${this.logPrefix()} Reservation with ID ${
1057 reservation.id
1058 } reached expiration date and was removed from CS`
1059 );
1060 }
1061 }
24578c31 1062 }
d193a949
JB
1063 }, interval);
1064 }
1065
1066 public restartReservationExpiryDateSetInterval(): void {
1067 this.stopReservationExpiryDateSetInterval();
1068 this.startReservationExpiryDateSetInterval();
1069 }
1070
1071 public validateIncomingRequestWithReservation(connectorId: number, idTag: string): boolean {
1072 const reservation = this.getReservationByConnectorId(connectorId);
1073 return Utils.isUndefined(reservation) || reservation.idTag !== idTag;
1074 }
1075
1076 public isConnectorReservable(
1077 reservationId: number,
1078 connectorId?: number,
1079 idTag?: string
1080 ): boolean {
1081 const [alreadyExists, _] = this.doesReservationExists({ id: reservationId });
1082 if (alreadyExists) {
1083 return alreadyExists;
24578c31 1084 }
d193a949
JB
1085 const userReservedAlready = Utils.isUndefined(this.getReservationByIdTag(idTag)) ? false : true;
1086 const notConnectorZero = Utils.isUndefined(connectorId) ? true : connectorId > 0;
1087 const freeConnectorsAvailable = this.getNumberOfReservableConnectors() > 0;
1088 return !alreadyExists && !userReservedAlready && notConnectorZero && freeConnectorsAvailable;
1089 }
1090
1091 private getNumberOfReservableConnectors(): number {
1092 let reservableConnectors = 0;
1093 this.connectors.forEach((connector, id) => {
1094 if (id === 0) {
1095 return;
1096 }
1097 if (connector.status === ConnectorStatusEnum.Available) {
1098 reservableConnectors++;
1099 }
1100 });
1101 return reservableConnectors - this.getNumberOfReservationsOnConnectorZero();
1102 }
1103
1104 private getNumberOfReservationsOnConnectorZero(): number {
1105 const reservations = this.reservations?.filter((reservation) => reservation.connectorId === 0);
1106 return Utils.isNullOrUndefined(reservations) ? 0 : reservations.length;
24578c31
JB
1107 }
1108
f90c1757 1109 private flushMessageBuffer(): void {
8e242273 1110 if (this.messageBuffer.size > 0) {
7d3b0f64 1111 for (const message of this.messageBuffer.values()) {
1431af78
JB
1112 let beginId: string;
1113 let commandName: RequestCommand;
8ca6874c 1114 const [messageType] = JSON.parse(message) as OutgoingRequest | Response | ErrorResponse;
1431af78
JB
1115 const isRequest = messageType === MessageType.CALL_MESSAGE;
1116 if (isRequest) {
1117 [, , commandName] = JSON.parse(message) as OutgoingRequest;
1118 beginId = PerformanceStatistics.beginMeasure(commandName);
1119 }
72092cfc 1120 this.wsConnection?.send(message);
1431af78 1121 isRequest && PerformanceStatistics.endMeasure(commandName, beginId);
8ca6874c
JB
1122 logger.debug(
1123 `${this.logPrefix()} >> Buffered ${OCPPServiceUtils.getMessageTypeString(
1124 messageType
1125 )} payload sent: ${message}`
1126 );
8e242273 1127 this.messageBuffer.delete(message);
7d3b0f64 1128 }
77f00f84
JB
1129 }
1130 }
1131
1f5df42a
JB
1132 private getSupervisionUrlOcppConfiguration(): boolean {
1133 return this.stationInfo.supervisionUrlOcppConfiguration ?? false;
12fc74d6
JB
1134 }
1135
d193a949
JB
1136 private stopReservationExpiryDateSetInterval(): void {
1137 if (this.reservationExpiryDateSetInterval) {
1138 clearInterval(this.reservationExpiryDateSetInterval);
1139 }
24578c31
JB
1140 }
1141
e8e865ea 1142 private getSupervisionUrlOcppKey(): string {
6dad8e21 1143 return this.stationInfo.supervisionUrlOcppKey ?? VendorParametersKey.ConnectionUrl;
e8e865ea
JB
1144 }
1145
72092cfc
JB
1146 private getTemplateFromFile(): ChargingStationTemplate | undefined {
1147 let template: ChargingStationTemplate;
5ad8570f 1148 try {
cda5d0fb
JB
1149 if (this.sharedLRUCache.hasChargingStationTemplate(this.templateFileHash)) {
1150 template = this.sharedLRUCache.getChargingStationTemplate(this.templateFileHash);
7c72977b
JB
1151 } else {
1152 const measureId = `${FileType.ChargingStationTemplate} read`;
1153 const beginId = PerformanceStatistics.beginMeasure(measureId);
1154 template = JSON.parse(
1155 fs.readFileSync(this.templateFile, 'utf8')
1156 ) as ChargingStationTemplate;
1157 PerformanceStatistics.endMeasure(measureId, beginId);
1158 template.templateHash = crypto
1159 .createHash(Constants.DEFAULT_HASH_ALGORITHM)
1160 .update(JSON.stringify(template))
1161 .digest('hex');
57adbebc 1162 this.sharedLRUCache.setChargingStationTemplate(template);
cda5d0fb 1163 this.templateFileHash = template.templateHash;
7c72977b 1164 }
5ad8570f 1165 } catch (error) {
51022aa0 1166 ErrorUtils.handleFileException(
2484ac1e 1167 this.templateFile,
7164966d
JB
1168 FileType.ChargingStationTemplate,
1169 error as NodeJS.ErrnoException,
1170 this.logPrefix()
e7aeea18 1171 );
5ad8570f 1172 }
2484ac1e
JB
1173 return template;
1174 }
1175
7a3a2ebb 1176 private getStationInfoFromTemplate(): ChargingStationInfo {
72092cfc 1177 const stationTemplate: ChargingStationTemplate | undefined = this.getTemplateFromFile();
d193a949 1178 // eslint-disable-next-line @typescript-eslint/no-unsafe-call
e1893686 1179 ChargingStationUtils.checkTemplate(stationTemplate, this.logPrefix(), this.templateFile);
ae5020a3 1180 ChargingStationUtils.warnTemplateKeysDeprecation(
ae5020a3 1181 stationTemplate,
8a133cc8
JB
1182 this.logPrefix(),
1183 this.templateFile
ae5020a3 1184 );
8a133cc8
JB
1185 if (stationTemplate?.Connectors) {
1186 ChargingStationUtils.checkConnectorsConfiguration(
1187 stationTemplate,
1188 this.logPrefix(),
1189 this.templateFile
1190 );
1191 }
fa7bccf4
JB
1192 const stationInfo: ChargingStationInfo =
1193 ChargingStationUtils.stationTemplateToStationInfo(stationTemplate);
51c83d6f 1194 stationInfo.hashId = ChargingStationUtils.getHashId(this.index, stationTemplate);
fa7bccf4
JB
1195 stationInfo.chargingStationId = ChargingStationUtils.getChargingStationId(
1196 this.index,
1197 stationTemplate
1198 );
72092cfc 1199 stationInfo.ocppVersion = stationTemplate?.ocppVersion ?? OCPPVersion.VERSION_16;
fa7bccf4 1200 ChargingStationUtils.createSerialNumber(stationTemplate, stationInfo);
53ac516c 1201 if (Utils.isNotEmptyArray(stationTemplate?.power)) {
551e477c 1202 stationTemplate.power = stationTemplate.power as number[];
fa7bccf4 1203 const powerArrayRandomIndex = Math.floor(Utils.secureRandom() * stationTemplate.power.length);
cc6e8ab5 1204 stationInfo.maximumPower =
72092cfc 1205 stationTemplate?.powerUnit === PowerUnits.KILO_WATT
fa7bccf4
JB
1206 ? stationTemplate.power[powerArrayRandomIndex] * 1000
1207 : stationTemplate.power[powerArrayRandomIndex];
5ad8570f 1208 } else {
551e477c 1209 stationTemplate.power = stationTemplate?.power as number;
cc6e8ab5 1210 stationInfo.maximumPower =
72092cfc 1211 stationTemplate?.powerUnit === PowerUnits.KILO_WATT
fa7bccf4
JB
1212 ? stationTemplate.power * 1000
1213 : stationTemplate.power;
1214 }
3637ca2c 1215 stationInfo.firmwareVersionPattern =
72092cfc 1216 stationTemplate?.firmwareVersionPattern ?? Constants.SEMVER_PATTERN;
3637ca2c 1217 if (
5a2a53cf 1218 Utils.isNotEmptyString(stationInfo.firmwareVersion) &&
3637ca2c
JB
1219 new RegExp(stationInfo.firmwareVersionPattern).test(stationInfo.firmwareVersion) === false
1220 ) {
1221 logger.warn(
1222 `${this.logPrefix()} Firmware version '${stationInfo.firmwareVersion}' in template file ${
1223 this.templateFile
1224 } does not match firmware version pattern '${stationInfo.firmwareVersionPattern}'`
1225 );
1226 }
598c886d 1227 stationInfo.firmwareUpgrade = merge<FirmwareUpgrade>(
15748260 1228 {
598c886d
JB
1229 versionUpgrade: {
1230 step: 1,
1231 },
15748260
JB
1232 reset: true,
1233 },
abe9e9dd 1234 stationTemplate?.firmwareUpgrade ?? {}
15748260 1235 );
d812bdcb 1236 stationInfo.resetTime = !Utils.isNullOrUndefined(stationTemplate?.resetTime)
fa7bccf4 1237 ? stationTemplate.resetTime * 1000
e7aeea18 1238 : Constants.CHARGING_STATION_DEFAULT_RESET_TIME;
fa7bccf4 1239 stationInfo.maximumAmperage = this.getMaximumAmperage(stationInfo);
9ac86a7e 1240 return stationInfo;
5ad8570f
JB
1241 }
1242
551e477c
JB
1243 private getStationInfoFromFile(): ChargingStationInfo | undefined {
1244 let stationInfo: ChargingStationInfo | undefined;
f832e5df
JB
1245 if (this.getStationInfoPersistentConfiguration()) {
1246 stationInfo = this.getConfigurationFromFile()?.stationInfo;
1247 if (stationInfo) {
1248 delete stationInfo?.infoHash;
1249 }
1250 }
f765beaa 1251 return stationInfo;
2484ac1e
JB
1252 }
1253
1254 private getStationInfo(): ChargingStationInfo {
1255 const stationInfoFromTemplate: ChargingStationInfo = this.getStationInfoFromTemplate();
551e477c 1256 const stationInfoFromFile: ChargingStationInfo | undefined = this.getStationInfoFromFile();
6b90dcca
JB
1257 // Priority:
1258 // 1. charging station info from template
1259 // 2. charging station info from configuration file
f765beaa 1260 if (stationInfoFromFile?.templateHash === stationInfoFromTemplate.templateHash) {
2484ac1e 1261 return stationInfoFromFile;
f765beaa 1262 }
fec4d204
JB
1263 stationInfoFromFile &&
1264 ChargingStationUtils.propagateSerialNumber(
1265 this.getTemplateFromFile(),
1266 stationInfoFromFile,
1267 stationInfoFromTemplate
1268 );
01efc60a 1269 return stationInfoFromTemplate;
2484ac1e
JB
1270 }
1271
1272 private saveStationInfo(): void {
ccb1d6e9 1273 if (this.getStationInfoPersistentConfiguration()) {
b1bbdae5 1274 this.saveConfiguration();
ccb1d6e9 1275 }
2484ac1e
JB
1276 }
1277
e8e865ea 1278 private getOcppPersistentConfiguration(): boolean {
ccb1d6e9
JB
1279 return this.stationInfo?.ocppPersistentConfiguration ?? true;
1280 }
1281
1282 private getStationInfoPersistentConfiguration(): boolean {
1283 return this.stationInfo?.stationInfoPersistentConfiguration ?? true;
e8e865ea
JB
1284 }
1285
c0560973 1286 private handleUnsupportedVersion(version: OCPPVersion) {
ded57f02
JB
1287 const errorMsg = `Unsupported protocol version '${version}' configured in template file ${this.templateFile}`;
1288 logger.error(`${this.logPrefix()} ${errorMsg}`);
1289 throw new BaseError(errorMsg);
c0560973
JB
1290 }
1291
2484ac1e 1292 private initialize(): void {
cda5d0fb 1293 const stationTemplate = this.getTemplateFromFile();
d193a949 1294 // eslint-disable-next-line @typescript-eslint/no-unsafe-call
e1893686 1295 ChargingStationUtils.checkTemplate(stationTemplate, this.logPrefix(), this.templateFile);
fa7bccf4 1296 this.configurationFile = path.join(
ee5f26a2 1297 path.dirname(this.templateFile.replace('station-templates', 'configurations')),
cda5d0fb 1298 `${ChargingStationUtils.getHashId(this.index, stationTemplate)}.json`
0642c3d2 1299 );
a4f7c75f
JB
1300 const chargingStationConfiguration = this.getConfigurationFromFile();
1301 const featureFlag = false;
1302 if (
1303 featureFlag &&
1304 (chargingStationConfiguration?.connectorsStatus || chargingStationConfiguration?.evsesStatus)
1305 ) {
1306 this.initializeConnectorsOrEvsesFromFile(chargingStationConfiguration);
1307 } else {
1308 this.initializeConnectorsOrEvsesFromTemplate(stationTemplate);
1309 }
b44b779a 1310 this.stationInfo = this.getStationInfo();
3637ca2c
JB
1311 if (
1312 this.stationInfo.firmwareStatus === FirmwareStatus.Installing &&
5a2a53cf
JB
1313 Utils.isNotEmptyString(this.stationInfo.firmwareVersion) &&
1314 Utils.isNotEmptyString(this.stationInfo.firmwareVersionPattern)
3637ca2c 1315 ) {
d812bdcb 1316 const patternGroup: number | undefined =
15748260 1317 this.stationInfo.firmwareUpgrade?.versionUpgrade?.patternGroup ??
d812bdcb 1318 this.stationInfo.firmwareVersion?.split('.').length;
72092cfc
JB
1319 const match = this.stationInfo?.firmwareVersion
1320 ?.match(new RegExp(this.stationInfo.firmwareVersionPattern))
1321 ?.slice(1, patternGroup + 1);
3637ca2c 1322 const patchLevelIndex = match.length - 1;
5d280aae 1323 match[patchLevelIndex] = (
07c52a72
JB
1324 Utils.convertToInt(match[patchLevelIndex]) +
1325 this.stationInfo.firmwareUpgrade?.versionUpgrade?.step
5d280aae 1326 ).toString();
72092cfc 1327 this.stationInfo.firmwareVersion = match?.join('.');
3637ca2c 1328 }
6bccfcbc 1329 this.saveStationInfo();
6bccfcbc
JB
1330 this.configuredSupervisionUrl = this.getConfiguredSupervisionUrl();
1331 if (this.getEnableStatistics() === true) {
1332 this.performanceStatistics = PerformanceStatistics.getInstance(
1333 this.stationInfo.hashId,
1334 this.stationInfo.chargingStationId,
1335 this.configuredSupervisionUrl
1336 );
1337 }
692f2f64
JB
1338 this.bootNotificationRequest = ChargingStationUtils.createBootNotificationRequest(
1339 this.stationInfo
1340 );
1341 this.powerDivider = this.getPowerDivider();
1342 // OCPP configuration
1343 this.ocppConfiguration = this.getOcppConfiguration();
1344 this.initializeOcppConfiguration();
1345 this.initializeOcppServices();
1346 if (this.stationInfo?.autoRegister === true) {
1347 this.bootNotificationResponse = {
1348 currentTime: new Date(),
1349 interval: this.getHeartbeatInterval() / 1000,
1350 status: RegistrationStatusEnumType.ACCEPTED,
1351 };
1352 }
147d0e0f
JB
1353 }
1354
feff11ec
JB
1355 private initializeOcppServices(): void {
1356 const ocppVersion = this.stationInfo.ocppVersion ?? OCPPVersion.VERSION_16;
1357 switch (ocppVersion) {
1358 case OCPPVersion.VERSION_16:
1359 this.ocppIncomingRequestService =
1360 OCPP16IncomingRequestService.getInstance<OCPP16IncomingRequestService>();
1361 this.ocppRequestService = OCPP16RequestService.getInstance<OCPP16RequestService>(
1362 OCPP16ResponseService.getInstance<OCPP16ResponseService>()
1363 );
1364 break;
1365 case OCPPVersion.VERSION_20:
1366 case OCPPVersion.VERSION_201:
1367 this.ocppIncomingRequestService =
1368 OCPP20IncomingRequestService.getInstance<OCPP20IncomingRequestService>();
1369 this.ocppRequestService = OCPP20RequestService.getInstance<OCPP20RequestService>(
1370 OCPP20ResponseService.getInstance<OCPP20ResponseService>()
1371 );
1372 break;
1373 default:
1374 this.handleUnsupportedVersion(ocppVersion);
1375 break;
1376 }
1377 }
1378
2484ac1e 1379 private initializeOcppConfiguration(): void {
17ac262c
JB
1380 if (
1381 !ChargingStationConfigurationUtils.getConfigurationKey(
1382 this,
1383 StandardParametersKey.HeartbeatInterval
1384 )
1385 ) {
1386 ChargingStationConfigurationUtils.addConfigurationKey(
1387 this,
1388 StandardParametersKey.HeartbeatInterval,
1389 '0'
1390 );
f0f65a62 1391 }
17ac262c
JB
1392 if (
1393 !ChargingStationConfigurationUtils.getConfigurationKey(
1394 this,
1395 StandardParametersKey.HeartBeatInterval
1396 )
1397 ) {
1398 ChargingStationConfigurationUtils.addConfigurationKey(
1399 this,
1400 StandardParametersKey.HeartBeatInterval,
1401 '0',
1402 { visible: false }
1403 );
f0f65a62 1404 }
e7aeea18
JB
1405 if (
1406 this.getSupervisionUrlOcppConfiguration() &&
269de583 1407 Utils.isNotEmptyString(this.getSupervisionUrlOcppKey()) &&
17ac262c 1408 !ChargingStationConfigurationUtils.getConfigurationKey(this, this.getSupervisionUrlOcppKey())
e7aeea18 1409 ) {
17ac262c
JB
1410 ChargingStationConfigurationUtils.addConfigurationKey(
1411 this,
a59737e3 1412 this.getSupervisionUrlOcppKey(),
fa7bccf4 1413 this.configuredSupervisionUrl.href,
e7aeea18
JB
1414 { reboot: true }
1415 );
e6895390
JB
1416 } else if (
1417 !this.getSupervisionUrlOcppConfiguration() &&
269de583 1418 Utils.isNotEmptyString(this.getSupervisionUrlOcppKey()) &&
17ac262c 1419 ChargingStationConfigurationUtils.getConfigurationKey(this, this.getSupervisionUrlOcppKey())
e6895390 1420 ) {
17ac262c
JB
1421 ChargingStationConfigurationUtils.deleteConfigurationKey(
1422 this,
1423 this.getSupervisionUrlOcppKey(),
1424 { save: false }
1425 );
12fc74d6 1426 }
cc6e8ab5 1427 if (
5a2a53cf 1428 Utils.isNotEmptyString(this.stationInfo?.amperageLimitationOcppKey) &&
17ac262c
JB
1429 !ChargingStationConfigurationUtils.getConfigurationKey(
1430 this,
1431 this.stationInfo.amperageLimitationOcppKey
1432 )
cc6e8ab5 1433 ) {
17ac262c
JB
1434 ChargingStationConfigurationUtils.addConfigurationKey(
1435 this,
cc6e8ab5 1436 this.stationInfo.amperageLimitationOcppKey,
17ac262c
JB
1437 (
1438 this.stationInfo.maximumAmperage *
1439 ChargingStationUtils.getAmperageLimitationUnitDivider(this.stationInfo)
1440 ).toString()
cc6e8ab5
JB
1441 );
1442 }
17ac262c
JB
1443 if (
1444 !ChargingStationConfigurationUtils.getConfigurationKey(
1445 this,
1446 StandardParametersKey.SupportedFeatureProfiles
1447 )
1448 ) {
1449 ChargingStationConfigurationUtils.addConfigurationKey(
1450 this,
e7aeea18 1451 StandardParametersKey.SupportedFeatureProfiles,
b22787b4 1452 `${SupportedFeatureProfiles.Core},${SupportedFeatureProfiles.FirmwareManagement},${SupportedFeatureProfiles.LocalAuthListManagement},${SupportedFeatureProfiles.SmartCharging},${SupportedFeatureProfiles.RemoteTrigger}`
e7aeea18
JB
1453 );
1454 }
17ac262c
JB
1455 ChargingStationConfigurationUtils.addConfigurationKey(
1456 this,
e7aeea18
JB
1457 StandardParametersKey.NumberOfConnectors,
1458 this.getNumberOfConnectors().toString(),
a95873d8
JB
1459 { readonly: true },
1460 { overwrite: true }
e7aeea18 1461 );
17ac262c
JB
1462 if (
1463 !ChargingStationConfigurationUtils.getConfigurationKey(
1464 this,
1465 StandardParametersKey.MeterValuesSampledData
1466 )
1467 ) {
1468 ChargingStationConfigurationUtils.addConfigurationKey(
1469 this,
e7aeea18
JB
1470 StandardParametersKey.MeterValuesSampledData,
1471 MeterValueMeasurand.ENERGY_ACTIVE_IMPORT_REGISTER
1472 );
7abfea5f 1473 }
17ac262c
JB
1474 if (
1475 !ChargingStationConfigurationUtils.getConfigurationKey(
1476 this,
1477 StandardParametersKey.ConnectorPhaseRotation
1478 )
1479 ) {
dd08d43d 1480 const connectorsPhaseRotation: string[] = [];
28e78158
JB
1481 if (this.hasEvses) {
1482 for (const evseStatus of this.evses.values()) {
1483 for (const connectorId of evseStatus.connectors.keys()) {
dd08d43d
JB
1484 connectorsPhaseRotation.push(
1485 ChargingStationUtils.getPhaseRotationValue(connectorId, this.getNumberOfPhases())
1486 );
28e78158
JB
1487 }
1488 }
1489 } else {
1490 for (const connectorId of this.connectors.keys()) {
dd08d43d
JB
1491 connectorsPhaseRotation.push(
1492 ChargingStationUtils.getPhaseRotationValue(connectorId, this.getNumberOfPhases())
1493 );
7e1dc878
JB
1494 }
1495 }
17ac262c
JB
1496 ChargingStationConfigurationUtils.addConfigurationKey(
1497 this,
e7aeea18 1498 StandardParametersKey.ConnectorPhaseRotation,
dd08d43d 1499 connectorsPhaseRotation.toString()
e7aeea18 1500 );
7e1dc878 1501 }
e7aeea18 1502 if (
17ac262c
JB
1503 !ChargingStationConfigurationUtils.getConfigurationKey(
1504 this,
1505 StandardParametersKey.AuthorizeRemoteTxRequests
e7aeea18
JB
1506 )
1507 ) {
17ac262c
JB
1508 ChargingStationConfigurationUtils.addConfigurationKey(
1509 this,
1510 StandardParametersKey.AuthorizeRemoteTxRequests,
1511 'true'
1512 );
36f6a92e 1513 }
17ac262c
JB
1514 if (
1515 !ChargingStationConfigurationUtils.getConfigurationKey(
1516 this,
1517 StandardParametersKey.LocalAuthListEnabled
1518 ) &&
1519 ChargingStationConfigurationUtils.getConfigurationKey(
1520 this,
1521 StandardParametersKey.SupportedFeatureProfiles
72092cfc 1522 )?.value?.includes(SupportedFeatureProfiles.LocalAuthListManagement)
17ac262c
JB
1523 ) {
1524 ChargingStationConfigurationUtils.addConfigurationKey(
1525 this,
1526 StandardParametersKey.LocalAuthListEnabled,
1527 'false'
1528 );
1529 }
1530 if (
1531 !ChargingStationConfigurationUtils.getConfigurationKey(
1532 this,
1533 StandardParametersKey.ConnectionTimeOut
1534 )
1535 ) {
1536 ChargingStationConfigurationUtils.addConfigurationKey(
1537 this,
e7aeea18
JB
1538 StandardParametersKey.ConnectionTimeOut,
1539 Constants.DEFAULT_CONNECTION_TIMEOUT.toString()
1540 );
8bce55bf 1541 }
2484ac1e 1542 this.saveOcppConfiguration();
073bd098
JB
1543 }
1544
a4f7c75f
JB
1545 private initializeConnectorsOrEvsesFromFile(configuration: ChargingStationConfiguration): void {
1546 if (configuration?.connectorsStatus && !configuration?.evsesStatus) {
8df5ae48
JB
1547 for (const [connectorId, connectorStatus] of configuration.connectorsStatus.entries()) {
1548 this.connectors.set(connectorId, Utils.cloneObject<ConnectorStatus>(connectorStatus));
1549 }
a4f7c75f
JB
1550 } else if (configuration?.evsesStatus && !configuration?.connectorsStatus) {
1551 for (const [evseId, evseStatusConfiguration] of configuration.evsesStatus.entries()) {
8df5ae48 1552 const evseStatus = Utils.cloneObject<EvseStatusConfiguration>(evseStatusConfiguration);
a4f7c75f
JB
1553 delete evseStatus.connectorsStatus;
1554 this.evses.set(evseId, {
8df5ae48 1555 ...(evseStatus as EvseStatus),
a4f7c75f
JB
1556 connectors: new Map<number, ConnectorStatus>(
1557 evseStatusConfiguration.connectorsStatus.map((connectorStatus, connectorId) => [
1558 connectorId,
1559 connectorStatus,
1560 ])
1561 ),
1562 });
1563 }
1564 } else if (configuration?.evsesStatus && configuration?.connectorsStatus) {
1565 const errorMsg = `Connectors and evses defined at the same time in configuration file ${this.configurationFile}`;
1566 logger.error(`${this.logPrefix()} ${errorMsg}`);
1567 throw new BaseError(errorMsg);
1568 } else {
1569 const errorMsg = `No connectors or evses defined in configuration file ${this.configurationFile}`;
1570 logger.error(`${this.logPrefix()} ${errorMsg}`);
1571 throw new BaseError(errorMsg);
1572 }
1573 }
1574
34eeb1fb 1575 private initializeConnectorsOrEvsesFromTemplate(stationTemplate: ChargingStationTemplate) {
cda5d0fb 1576 if (stationTemplate?.Connectors && !stationTemplate?.Evses) {
34eeb1fb 1577 this.initializeConnectorsFromTemplate(stationTemplate);
cda5d0fb 1578 } else if (stationTemplate?.Evses && !stationTemplate?.Connectors) {
34eeb1fb 1579 this.initializeEvsesFromTemplate(stationTemplate);
cda5d0fb 1580 } else if (stationTemplate?.Evses && stationTemplate?.Connectors) {
ae25f265
JB
1581 const errorMsg = `Connectors and evses defined at the same time in template file ${this.templateFile}`;
1582 logger.error(`${this.logPrefix()} ${errorMsg}`);
1583 throw new BaseError(errorMsg);
1584 } else {
1585 const errorMsg = `No connectors or evses defined in template file ${this.templateFile}`;
1586 logger.error(`${this.logPrefix()} ${errorMsg}`);
1587 throw new BaseError(errorMsg);
1588 }
1589 }
1590
34eeb1fb 1591 private initializeConnectorsFromTemplate(stationTemplate: ChargingStationTemplate): void {
cda5d0fb 1592 if (!stationTemplate?.Connectors && this.connectors.size === 0) {
ded57f02
JB
1593 const errorMsg = `No already defined connectors and charging station information from template ${this.templateFile} with no connectors configuration defined`;
1594 logger.error(`${this.logPrefix()} ${errorMsg}`);
1595 throw new BaseError(errorMsg);
3d25cc86 1596 }
cda5d0fb 1597 if (!stationTemplate?.Connectors[0]) {
3d25cc86
JB
1598 logger.warn(
1599 `${this.logPrefix()} Charging station information from template ${
1600 this.templateFile
2585c6e9 1601 } with no connector id 0 configuration`
3d25cc86
JB
1602 );
1603 }
cda5d0fb
JB
1604 if (stationTemplate?.Connectors) {
1605 const { configuredMaxConnectors, templateMaxConnectors, templateMaxAvailableConnectors } =
1606 ChargingStationUtils.checkConnectorsConfiguration(
1607 stationTemplate,
8a133cc8
JB
1608 this.logPrefix(),
1609 this.templateFile
cda5d0fb 1610 );
3d25cc86
JB
1611 const connectorsConfigHash = crypto
1612 .createHash(Constants.DEFAULT_HASH_ALGORITHM)
cda5d0fb
JB
1613 .update(
1614 `${JSON.stringify(stationTemplate?.Connectors)}${configuredMaxConnectors.toString()}`
1615 )
3d25cc86
JB
1616 .digest('hex');
1617 const connectorsConfigChanged =
1618 this.connectors?.size !== 0 && this.connectorsConfigurationHash !== connectorsConfigHash;
1619 if (this.connectors?.size === 0 || connectorsConfigChanged) {
1620 connectorsConfigChanged && this.connectors.clear();
1621 this.connectorsConfigurationHash = connectorsConfigHash;
269196a8
JB
1622 if (templateMaxConnectors > 0) {
1623 for (let connectorId = 0; connectorId <= configuredMaxConnectors; connectorId++) {
1624 if (
1625 connectorId === 0 &&
cda5d0fb
JB
1626 (!stationTemplate?.Connectors[connectorId] ||
1627 this.getUseConnectorId0(stationTemplate) === false)
269196a8
JB
1628 ) {
1629 continue;
1630 }
1631 const templateConnectorId =
cda5d0fb 1632 connectorId > 0 && stationTemplate?.randomConnectors
269196a8
JB
1633 ? Utils.getRandomInteger(templateMaxAvailableConnectors, 1)
1634 : connectorId;
cda5d0fb 1635 const connectorStatus = stationTemplate?.Connectors[templateConnectorId];
04b1261c 1636 ChargingStationUtils.checkStationInfoConnectorStatus(
ae25f265 1637 templateConnectorId,
04b1261c
JB
1638 connectorStatus,
1639 this.logPrefix(),
1640 this.templateFile
1641 );
ae25f265 1642 this.connectors.set(connectorId, Utils.cloneObject<ConnectorStatus>(connectorStatus));
3d25cc86 1643 }
52952bf8
JB
1644 ChargingStationUtils.initializeConnectorsMapStatus(this.connectors, this.logPrefix());
1645 this.saveConnectorsStatus();
ae25f265
JB
1646 } else {
1647 logger.warn(
1648 `${this.logPrefix()} Charging station information from template ${
1649 this.templateFile
1650 } with no connectors configuration defined, cannot create connectors`
1651 );
3d25cc86
JB
1652 }
1653 }
1654 } else {
1655 logger.warn(
1656 `${this.logPrefix()} Charging station information from template ${
1657 this.templateFile
1658 } with no connectors configuration defined, using already defined connectors`
1659 );
1660 }
3d25cc86
JB
1661 }
1662
34eeb1fb 1663 private initializeEvsesFromTemplate(stationTemplate: ChargingStationTemplate): void {
cda5d0fb 1664 if (!stationTemplate?.Evses && this.evses.size === 0) {
ded57f02
JB
1665 const errorMsg = `No already defined evses and charging station information from template ${this.templateFile} with no evses configuration defined`;
1666 logger.error(`${this.logPrefix()} ${errorMsg}`);
1667 throw new BaseError(errorMsg);
2585c6e9 1668 }
cda5d0fb 1669 if (!stationTemplate?.Evses[0]) {
2585c6e9
JB
1670 logger.warn(
1671 `${this.logPrefix()} Charging station information from template ${
1672 this.templateFile
1673 } with no evse id 0 configuration`
1674 );
1675 }
cda5d0fb 1676 if (!stationTemplate?.Evses[0]?.Connectors[0]) {
59a0f26d
JB
1677 logger.warn(
1678 `${this.logPrefix()} Charging station information from template ${
1679 this.templateFile
1680 } with evse id 0 with no connector id 0 configuration`
1681 );
1682 }
cda5d0fb 1683 if (stationTemplate?.Evses) {
2585c6e9
JB
1684 const evsesConfigHash = crypto
1685 .createHash(Constants.DEFAULT_HASH_ALGORITHM)
cda5d0fb 1686 .update(`${JSON.stringify(stationTemplate?.Evses)}`)
2585c6e9
JB
1687 .digest('hex');
1688 const evsesConfigChanged =
1689 this.evses?.size !== 0 && this.evsesConfigurationHash !== evsesConfigHash;
1690 if (this.evses?.size === 0 || evsesConfigChanged) {
1691 evsesConfigChanged && this.evses.clear();
1692 this.evsesConfigurationHash = evsesConfigHash;
cda5d0fb 1693 const templateMaxEvses = ChargingStationUtils.getMaxNumberOfEvses(stationTemplate?.Evses);
ae25f265 1694 if (templateMaxEvses > 0) {
cda5d0fb 1695 for (const evse in stationTemplate.Evses) {
52952bf8
JB
1696 const evseId = Utils.convertToInt(evse);
1697 this.evses.set(evseId, {
ae25f265 1698 connectors: ChargingStationUtils.buildConnectorsMap(
cda5d0fb 1699 stationTemplate?.Evses[evse]?.Connectors,
ae25f265
JB
1700 this.logPrefix(),
1701 this.templateFile
1702 ),
1703 availability: AvailabilityType.Operative,
1704 });
1705 ChargingStationUtils.initializeConnectorsMapStatus(
52952bf8 1706 this.evses.get(evseId)?.connectors,
ae25f265
JB
1707 this.logPrefix()
1708 );
1709 }
52952bf8 1710 this.saveEvsesStatus();
ae25f265
JB
1711 } else {
1712 logger.warn(
1713 `${this.logPrefix()} Charging station information from template ${
04b1261c 1714 this.templateFile
ae25f265 1715 } with no evses configuration defined, cannot create evses`
04b1261c 1716 );
2585c6e9
JB
1717 }
1718 }
513db108
JB
1719 } else {
1720 logger.warn(
1721 `${this.logPrefix()} Charging station information from template ${
1722 this.templateFile
1723 } with no evses configuration defined, using already defined evses`
1724 );
2585c6e9
JB
1725 }
1726 }
1727
551e477c
JB
1728 private getConfigurationFromFile(): ChargingStationConfiguration | undefined {
1729 let configuration: ChargingStationConfiguration | undefined;
7b5dbe91 1730 if (Utils.isNotEmptyString(this.configurationFile) && fs.existsSync(this.configurationFile)) {
073bd098 1731 try {
57adbebc
JB
1732 if (this.sharedLRUCache.hasChargingStationConfiguration(this.configurationFileHash)) {
1733 configuration = this.sharedLRUCache.getChargingStationConfiguration(
1734 this.configurationFileHash
1735 );
7c72977b
JB
1736 } else {
1737 const measureId = `${FileType.ChargingStationConfiguration} read`;
1738 const beginId = PerformanceStatistics.beginMeasure(measureId);
1739 configuration = JSON.parse(
1740 fs.readFileSync(this.configurationFile, 'utf8')
1741 ) as ChargingStationConfiguration;
1742 PerformanceStatistics.endMeasure(measureId, beginId);
57adbebc 1743 this.sharedLRUCache.setChargingStationConfiguration(configuration);
cda5d0fb 1744 this.configurationFileHash = configuration.configurationHash;
7c72977b 1745 }
073bd098 1746 } catch (error) {
51022aa0 1747 ErrorUtils.handleFileException(
073bd098 1748 this.configurationFile,
7164966d
JB
1749 FileType.ChargingStationConfiguration,
1750 error as NodeJS.ErrnoException,
1751 this.logPrefix()
073bd098
JB
1752 );
1753 }
1754 }
1755 return configuration;
1756 }
1757
179ed367
JB
1758 private saveChargingStationAutomaticTransactionGeneratorConfiguration(): void {
1759 this.saveConfiguration();
ac7f79af
JB
1760 }
1761
52952bf8 1762 private saveConnectorsStatus() {
7446de3b 1763 this.saveConfiguration();
52952bf8
JB
1764 }
1765
1766 private saveEvsesStatus() {
7446de3b 1767 this.saveConfiguration();
52952bf8
JB
1768 }
1769
179ed367 1770 private saveConfiguration(): void {
7b5dbe91 1771 if (Utils.isNotEmptyString(this.configurationFile)) {
2484ac1e 1772 try {
2484ac1e
JB
1773 if (!fs.existsSync(path.dirname(this.configurationFile))) {
1774 fs.mkdirSync(path.dirname(this.configurationFile), { recursive: true });
073bd098 1775 }
ac7f79af 1776 let configurationData: ChargingStationConfiguration =
8df5ae48 1777 Utils.cloneObject<ChargingStationConfiguration>(this.getConfigurationFromFile()) ?? {};
34eeb1fb 1778 if (this.getStationInfoPersistentConfiguration() && this.stationInfo) {
52952bf8
JB
1779 configurationData.stationInfo = this.stationInfo;
1780 }
34eeb1fb 1781 if (this.getOcppPersistentConfiguration() && this.ocppConfiguration?.configurationKey) {
52952bf8
JB
1782 configurationData.configurationKey = this.ocppConfiguration.configurationKey;
1783 }
179ed367
JB
1784 configurationData = merge<ChargingStationConfiguration>(
1785 configurationData,
1786 buildChargingStationAutomaticTransactionGeneratorConfiguration(this)
1787 );
b1bbdae5 1788 if (this.connectors.size > 0) {
179ed367 1789 configurationData.connectorsStatus = buildConnectorsStatus(this);
52952bf8 1790 }
b1bbdae5 1791 if (this.evses.size > 0) {
179ed367 1792 configurationData.evsesStatus = buildEvsesStatus(this);
52952bf8 1793 }
7c72977b
JB
1794 delete configurationData.configurationHash;
1795 const configurationHash = crypto
1796 .createHash(Constants.DEFAULT_HASH_ALGORITHM)
1797 .update(JSON.stringify(configurationData))
1798 .digest('hex');
1799 if (this.configurationFileHash !== configurationHash) {
dd485b56 1800 AsyncLock.acquire(AsyncLockType.configuration)
1227a6f1
JB
1801 .then(() => {
1802 configurationData.configurationHash = configurationHash;
1803 const measureId = `${FileType.ChargingStationConfiguration} write`;
1804 const beginId = PerformanceStatistics.beginMeasure(measureId);
1805 const fileDescriptor = fs.openSync(this.configurationFile, 'w');
1806 fs.writeFileSync(fileDescriptor, JSON.stringify(configurationData, null, 2), 'utf8');
1807 fs.closeSync(fileDescriptor);
1808 PerformanceStatistics.endMeasure(measureId, beginId);
1809 this.sharedLRUCache.deleteChargingStationConfiguration(this.configurationFileHash);
1810 this.sharedLRUCache.setChargingStationConfiguration(configurationData);
1811 this.configurationFileHash = configurationHash;
1812 })
1813 .catch((error) => {
51022aa0 1814 ErrorUtils.handleFileException(
1227a6f1
JB
1815 this.configurationFile,
1816 FileType.ChargingStationConfiguration,
1817 error as NodeJS.ErrnoException,
1818 this.logPrefix()
1819 );
1820 })
1821 .finally(() => {
dd485b56 1822 AsyncLock.release(AsyncLockType.configuration).catch(Constants.EMPTY_FUNCTION);
1227a6f1 1823 });
7c72977b
JB
1824 } else {
1825 logger.debug(
1826 `${this.logPrefix()} Not saving unchanged charging station configuration file ${
1827 this.configurationFile
1828 }`
1829 );
2484ac1e 1830 }
2484ac1e 1831 } catch (error) {
51022aa0 1832 ErrorUtils.handleFileException(
2484ac1e 1833 this.configurationFile,
7164966d
JB
1834 FileType.ChargingStationConfiguration,
1835 error as NodeJS.ErrnoException,
1836 this.logPrefix()
073bd098
JB
1837 );
1838 }
2484ac1e
JB
1839 } else {
1840 logger.error(
01efc60a 1841 `${this.logPrefix()} Trying to save charging station configuration to undefined configuration file`
2484ac1e 1842 );
073bd098
JB
1843 }
1844 }
1845
551e477c
JB
1846 private getOcppConfigurationFromTemplate(): ChargingStationOcppConfiguration | undefined {
1847 return this.getTemplateFromFile()?.Configuration;
2484ac1e
JB
1848 }
1849
551e477c 1850 private getOcppConfigurationFromFile(): ChargingStationOcppConfiguration | undefined {
60655b26
JB
1851 const configurationKey = this.getConfigurationFromFile()?.configurationKey;
1852 if (this.getOcppPersistentConfiguration() === true && configurationKey) {
1853 return { configurationKey };
648512ce 1854 }
60655b26 1855 return undefined;
7dde0b73
JB
1856 }
1857
551e477c
JB
1858 private getOcppConfiguration(): ChargingStationOcppConfiguration | undefined {
1859 let ocppConfiguration: ChargingStationOcppConfiguration | undefined =
72092cfc 1860 this.getOcppConfigurationFromFile();
2484ac1e
JB
1861 if (!ocppConfiguration) {
1862 ocppConfiguration = this.getOcppConfigurationFromTemplate();
1863 }
1864 return ocppConfiguration;
1865 }
1866
c0560973 1867 private async onOpen(): Promise<void> {
56eb297e 1868 if (this.isWebSocketConnectionOpened() === true) {
5144f4d1
JB
1869 logger.info(
1870 `${this.logPrefix()} Connection to OCPP server through ${this.wsConnectionUrl.toString()} succeeded`
1871 );
ed6cfcff 1872 if (this.isRegistered() === false) {
5144f4d1
JB
1873 // Send BootNotification
1874 let registrationRetryCount = 0;
1875 do {
f7f98c68 1876 this.bootNotificationResponse = await this.ocppRequestService.requestHandler<
5144f4d1
JB
1877 BootNotificationRequest,
1878 BootNotificationResponse
8bfbc743
JB
1879 >(this, RequestCommand.BOOT_NOTIFICATION, this.bootNotificationRequest, {
1880 skipBufferingOnError: true,
1881 });
ed6cfcff 1882 if (this.isRegistered() === false) {
5144f4d1
JB
1883 this.getRegistrationMaxRetries() !== -1 && registrationRetryCount++;
1884 await Utils.sleep(
1895299d 1885 this?.bootNotificationResponse?.interval
5144f4d1 1886 ? this.bootNotificationResponse.interval * 1000
7d34a2f4 1887 : Constants.DEFAULT_BOOT_NOTIFICATION_INTERVAL
5144f4d1
JB
1888 );
1889 }
1890 } while (
ed6cfcff 1891 this.isRegistered() === false &&
5144f4d1
JB
1892 (registrationRetryCount <= this.getRegistrationMaxRetries() ||
1893 this.getRegistrationMaxRetries() === -1)
1894 );
1895 }
ed6cfcff 1896 if (this.isRegistered() === true) {
f7c2994d 1897 if (this.inAcceptedState() === true) {
94bb24d5 1898 await this.startMessageSequence();
c0560973 1899 }
5144f4d1
JB
1900 } else {
1901 logger.error(
1902 `${this.logPrefix()} Registration failure: max retries reached (${this.getRegistrationMaxRetries()}) or retry disabled (${this.getRegistrationMaxRetries()})`
1903 );
caad9d6b 1904 }
5144f4d1 1905 this.wsConnectionRestarted = false;
aa428a31 1906 this.autoReconnectRetryCount = 0;
1895299d 1907 parentPort?.postMessage(MessageChannelUtils.buildUpdatedMessage(this));
2e6f5966 1908 } else {
5144f4d1
JB
1909 logger.warn(
1910 `${this.logPrefix()} Connection to OCPP server through ${this.wsConnectionUrl.toString()} failed`
e7aeea18 1911 );
2e6f5966 1912 }
2e6f5966
JB
1913 }
1914
ef7d8c21 1915 private async onClose(code: number, reason: Buffer): Promise<void> {
d09085e9 1916 switch (code) {
6c65a295
JB
1917 // Normal close
1918 case WebSocketCloseEventStatusCode.CLOSE_NORMAL:
c0560973 1919 case WebSocketCloseEventStatusCode.CLOSE_NO_STATUS:
e7aeea18 1920 logger.info(
5e3cb728 1921 `${this.logPrefix()} WebSocket normally closed with status '${Utils.getWebSocketCloseEventStatusString(
e7aeea18 1922 code
ef7d8c21 1923 )}' and reason '${reason.toString()}'`
e7aeea18 1924 );
c0560973
JB
1925 this.autoReconnectRetryCount = 0;
1926 break;
6c65a295
JB
1927 // Abnormal close
1928 default:
e7aeea18 1929 logger.error(
5e3cb728 1930 `${this.logPrefix()} WebSocket abnormally closed with status '${Utils.getWebSocketCloseEventStatusString(
e7aeea18 1931 code
ef7d8c21 1932 )}' and reason '${reason.toString()}'`
e7aeea18 1933 );
56eb297e 1934 this.started === true && (await this.reconnect());
c0560973
JB
1935 break;
1936 }
1895299d 1937 parentPort?.postMessage(MessageChannelUtils.buildUpdatedMessage(this));
2e6f5966
JB
1938 }
1939
56d09fd7
JB
1940 private getCachedRequest(messageType: MessageType, messageId: string): CachedRequest | undefined {
1941 const cachedRequest = this.requests.get(messageId);
1942 if (Array.isArray(cachedRequest) === true) {
1943 return cachedRequest;
1944 }
1945 throw new OCPPError(
1946 ErrorType.PROTOCOL_ERROR,
1947 `Cached request for message id ${messageId} ${OCPPServiceUtils.getMessageTypeString(
1948 messageType
1949 )} is not an array`,
1950 undefined,
617cad0c 1951 cachedRequest as JsonType
56d09fd7
JB
1952 );
1953 }
1954
1955 private async handleIncomingMessage(request: IncomingRequest): Promise<void> {
1956 const [messageType, messageId, commandName, commandPayload] = request;
1957 if (this.getEnableStatistics() === true) {
1958 this.performanceStatistics?.addRequestStatistic(commandName, messageType);
1959 }
1960 logger.debug(
1961 `${this.logPrefix()} << Command '${commandName}' received request payload: ${JSON.stringify(
1962 request
1963 )}`
1964 );
1965 // Process the message
1966 await this.ocppIncomingRequestService.incomingRequestHandler(
1967 this,
1968 messageId,
1969 commandName,
1970 commandPayload
1971 );
1972 }
1973
1974 private handleResponseMessage(response: Response): void {
1975 const [messageType, messageId, commandPayload] = response;
1976 if (this.requests.has(messageId) === false) {
1977 // Error
1978 throw new OCPPError(
1979 ErrorType.INTERNAL_ERROR,
1980 `Response for unknown message id ${messageId}`,
1981 undefined,
1982 commandPayload
1983 );
1984 }
1985 // Respond
1986 const [responseCallback, , requestCommandName, requestPayload] = this.getCachedRequest(
1987 messageType,
1988 messageId
1989 );
1990 logger.debug(
1991 `${this.logPrefix()} << Command '${
1992 requestCommandName ?? Constants.UNKNOWN_COMMAND
1993 }' received response payload: ${JSON.stringify(response)}`
1994 );
1995 responseCallback(commandPayload, requestPayload);
1996 }
1997
1998 private handleErrorMessage(errorResponse: ErrorResponse): void {
1999 const [messageType, messageId, errorType, errorMessage, errorDetails] = errorResponse;
2000 if (this.requests.has(messageId) === false) {
2001 // Error
2002 throw new OCPPError(
2003 ErrorType.INTERNAL_ERROR,
2004 `Error response for unknown message id ${messageId}`,
2005 undefined,
2006 { errorType, errorMessage, errorDetails }
2007 );
2008 }
2009 const [, errorCallback, requestCommandName] = this.getCachedRequest(messageType, messageId);
2010 logger.debug(
2011 `${this.logPrefix()} << Command '${
2012 requestCommandName ?? Constants.UNKNOWN_COMMAND
2013 }' received error response payload: ${JSON.stringify(errorResponse)}`
2014 );
2015 errorCallback(new OCPPError(errorType, errorMessage, requestCommandName, errorDetails));
2016 }
2017
ef7d8c21 2018 private async onMessage(data: RawData): Promise<void> {
56d09fd7 2019 let request: IncomingRequest | Response | ErrorResponse;
b3ec7bc1 2020 let messageType: number;
ded57f02 2021 let errorMsg: string;
c0560973 2022 try {
56d09fd7 2023 request = JSON.parse(data.toString()) as IncomingRequest | Response | ErrorResponse;
53e5fd67 2024 if (Array.isArray(request) === true) {
56d09fd7 2025 [messageType] = request;
b3ec7bc1
JB
2026 // Check the type of message
2027 switch (messageType) {
2028 // Incoming Message
2029 case MessageType.CALL_MESSAGE:
56d09fd7 2030 await this.handleIncomingMessage(request as IncomingRequest);
b3ec7bc1 2031 break;
56d09fd7 2032 // Response Message
b3ec7bc1 2033 case MessageType.CALL_RESULT_MESSAGE:
56d09fd7 2034 this.handleResponseMessage(request as Response);
a2d1c0f1
JB
2035 break;
2036 // Error Message
2037 case MessageType.CALL_ERROR_MESSAGE:
56d09fd7 2038 this.handleErrorMessage(request as ErrorResponse);
b3ec7bc1 2039 break;
56d09fd7 2040 // Unknown Message
b3ec7bc1
JB
2041 default:
2042 // eslint-disable-next-line @typescript-eslint/restrict-template-expressions
ded57f02
JB
2043 errorMsg = `Wrong message type ${messageType}`;
2044 logger.error(`${this.logPrefix()} ${errorMsg}`);
2045 throw new OCPPError(ErrorType.PROTOCOL_ERROR, errorMsg);
b3ec7bc1 2046 }
1895299d 2047 parentPort?.postMessage(MessageChannelUtils.buildUpdatedMessage(this));
47e22477 2048 } else {
53e5fd67 2049 throw new OCPPError(ErrorType.PROTOCOL_ERROR, 'Incoming message is not an array', null, {
ba7965c4 2050 request,
ac54a9bb 2051 });
47e22477 2052 }
c0560973 2053 } catch (error) {
56d09fd7
JB
2054 let commandName: IncomingRequestCommand;
2055 let requestCommandName: RequestCommand | IncomingRequestCommand;
2056 let errorCallback: ErrorCallback;
2057 const [, messageId] = request;
13701f69
JB
2058 switch (messageType) {
2059 case MessageType.CALL_MESSAGE:
56d09fd7 2060 [, , commandName] = request as IncomingRequest;
13701f69 2061 // Send error
56d09fd7 2062 await this.ocppRequestService.sendError(this, messageId, error as OCPPError, commandName);
13701f69
JB
2063 break;
2064 case MessageType.CALL_RESULT_MESSAGE:
2065 case MessageType.CALL_ERROR_MESSAGE:
56d09fd7
JB
2066 if (this.requests.has(messageId) === true) {
2067 [, errorCallback, requestCommandName] = this.getCachedRequest(messageType, messageId);
13701f69
JB
2068 // Reject the deferred promise in case of error at response handling (rejecting an already fulfilled promise is a no-op)
2069 errorCallback(error as OCPPError, false);
2070 } else {
2071 // Remove the request from the cache in case of error at response handling
2072 this.requests.delete(messageId);
2073 }
de4cb8b6 2074 break;
ba7965c4 2075 }
56d09fd7
JB
2076 if (error instanceof OCPPError === false) {
2077 logger.warn(
2078 `${this.logPrefix()} Error thrown at incoming OCPP command '${
2079 commandName ?? requestCommandName ?? Constants.UNKNOWN_COMMAND
2080 }' message '${data.toString()}' handling is not an OCPPError:`,
2081 error
2082 );
2083 }
2084 logger.error(
2085 `${this.logPrefix()} Incoming OCPP command '${
2086 commandName ?? requestCommandName ?? Constants.UNKNOWN_COMMAND
2087 }' message '${data.toString()}'${
2088 messageType !== MessageType.CALL_MESSAGE
2089 ? ` matching cached request '${JSON.stringify(this.requests.get(messageId))}'`
2090 : ''
2091 } processing error:`,
2092 error
2093 );
c0560973 2094 }
2328be1e
JB
2095 }
2096
c0560973 2097 private onPing(): void {
44eb6026 2098 logger.debug(`${this.logPrefix()} Received a WS ping (rfc6455) from the server`);
c0560973
JB
2099 }
2100
2101 private onPong(): void {
44eb6026 2102 logger.debug(`${this.logPrefix()} Received a WS pong (rfc6455) from the server`);
c0560973
JB
2103 }
2104
9534e74e 2105 private onError(error: WSError): void {
bcc9c3c0 2106 this.closeWSConnection();
44eb6026 2107 logger.error(`${this.logPrefix()} WebSocket error:`, error);
c0560973
JB
2108 }
2109
18bf8274 2110 private getEnergyActiveImportRegister(connectorStatus: ConnectorStatus, rounded = false): number {
95bdbf12 2111 if (this.getMeteringPerTransaction() === true) {
07989fad 2112 return (
18bf8274 2113 (rounded === true
07989fad
JB
2114 ? Math.round(connectorStatus?.transactionEnergyActiveImportRegisterValue)
2115 : connectorStatus?.transactionEnergyActiveImportRegisterValue) ?? 0
2116 );
2117 }
2118 return (
18bf8274 2119 (rounded === true
07989fad
JB
2120 ? Math.round(connectorStatus?.energyActiveImportRegisterValue)
2121 : connectorStatus?.energyActiveImportRegisterValue) ?? 0
2122 );
2123 }
2124
cda5d0fb
JB
2125 private getUseConnectorId0(stationTemplate?: ChargingStationTemplate): boolean {
2126 return stationTemplate?.useConnectorId0 ?? true;
8bce55bf
JB
2127 }
2128
60ddad53 2129 private async stopRunningTransactions(reason = StopTransactionReason.NONE): Promise<void> {
28e78158
JB
2130 if (this.hasEvses) {
2131 for (const evseStatus of this.evses.values()) {
2132 for (const [connectorId, connectorStatus] of evseStatus.connectors) {
2133 if (connectorStatus.transactionStarted === true) {
2134 await this.stopTransactionOnConnector(connectorId, reason);
2135 }
2136 }
2137 }
2138 } else {
2139 for (const connectorId of this.connectors.keys()) {
2140 if (connectorId > 0 && this.getConnectorStatus(connectorId)?.transactionStarted === true) {
2141 await this.stopTransactionOnConnector(connectorId, reason);
2142 }
60ddad53
JB
2143 }
2144 }
2145 }
2146
1f761b9a 2147 // 0 for disabling
c72f6634 2148 private getConnectionTimeout(): number {
17ac262c
JB
2149 if (
2150 ChargingStationConfigurationUtils.getConfigurationKey(
2151 this,
2152 StandardParametersKey.ConnectionTimeOut
2153 )
2154 ) {
e7aeea18 2155 return (
17ac262c
JB
2156 parseInt(
2157 ChargingStationConfigurationUtils.getConfigurationKey(
2158 this,
2159 StandardParametersKey.ConnectionTimeOut
2160 ).value
2161 ) ?? Constants.DEFAULT_CONNECTION_TIMEOUT
e7aeea18 2162 );
291cb255 2163 }
291cb255 2164 return Constants.DEFAULT_CONNECTION_TIMEOUT;
3574dfd3
JB
2165 }
2166
1f761b9a 2167 // -1 for unlimited, 0 for disabling
72092cfc 2168 private getAutoReconnectMaxRetries(): number | undefined {
b1bbdae5
JB
2169 return (
2170 this.stationInfo.autoReconnectMaxRetries ?? Configuration.getAutoReconnectMaxRetries() ?? -1
2171 );
3574dfd3
JB
2172 }
2173
ec977daf 2174 // 0 for disabling
72092cfc 2175 private getRegistrationMaxRetries(): number | undefined {
b1bbdae5 2176 return this.stationInfo.registrationMaxRetries ?? -1;
32a1eb7a
JB
2177 }
2178
c0560973 2179 private getPowerDivider(): number {
b1bbdae5 2180 let powerDivider = this.hasEvses ? this.getNumberOfEvses() : this.getNumberOfConnectors();
fa7bccf4 2181 if (this.stationInfo?.powerSharedByConnectors) {
c0560973 2182 powerDivider = this.getNumberOfRunningTransactions();
6ecb15e4
JB
2183 }
2184 return powerDivider;
2185 }
2186
fa7bccf4
JB
2187 private getMaximumAmperage(stationInfo: ChargingStationInfo): number | undefined {
2188 const maximumPower = this.getMaximumPower(stationInfo);
2189 switch (this.getCurrentOutType(stationInfo)) {
cc6e8ab5
JB
2190 case CurrentType.AC:
2191 return ACElectricUtils.amperagePerPhaseFromPower(
fa7bccf4 2192 this.getNumberOfPhases(stationInfo),
b1bbdae5 2193 maximumPower / (this.hasEvses ? this.getNumberOfEvses() : this.getNumberOfConnectors()),
fa7bccf4 2194 this.getVoltageOut(stationInfo)
cc6e8ab5
JB
2195 );
2196 case CurrentType.DC:
fa7bccf4 2197 return DCElectricUtils.amperage(maximumPower, this.getVoltageOut(stationInfo));
cc6e8ab5
JB
2198 }
2199 }
2200
cc6e8ab5
JB
2201 private getAmperageLimitation(): number | undefined {
2202 if (
5a2a53cf 2203 Utils.isNotEmptyString(this.stationInfo?.amperageLimitationOcppKey) &&
17ac262c
JB
2204 ChargingStationConfigurationUtils.getConfigurationKey(
2205 this,
2206 this.stationInfo.amperageLimitationOcppKey
2207 )
cc6e8ab5
JB
2208 ) {
2209 return (
2210 Utils.convertToInt(
17ac262c
JB
2211 ChargingStationConfigurationUtils.getConfigurationKey(
2212 this,
2213 this.stationInfo.amperageLimitationOcppKey
72092cfc 2214 )?.value
17ac262c 2215 ) / ChargingStationUtils.getAmperageLimitationUnitDivider(this.stationInfo)
cc6e8ab5
JB
2216 );
2217 }
2218 }
2219
c0560973 2220 private async startMessageSequence(): Promise<void> {
b7f9e41d 2221 if (this.stationInfo?.autoRegister === true) {
f7f98c68 2222 await this.ocppRequestService.requestHandler<
ef6fa3fb
JB
2223 BootNotificationRequest,
2224 BootNotificationResponse
8bfbc743
JB
2225 >(this, RequestCommand.BOOT_NOTIFICATION, this.bootNotificationRequest, {
2226 skipBufferingOnError: true,
2227 });
6114e6f1 2228 }
136c90ba 2229 // Start WebSocket ping
c0560973 2230 this.startWebSocketPing();
5ad8570f 2231 // Start heartbeat
c0560973 2232 this.startHeartbeat();
0a60c33c 2233 // Initialize connectors status
c3b83130
JB
2234 if (this.hasEvses) {
2235 for (const [evseId, evseStatus] of this.evses) {
4334db72
JB
2236 if (evseId > 0) {
2237 for (const [connectorId, connectorStatus] of evseStatus.connectors) {
2238 const connectorBootStatus = ChargingStationUtils.getBootConnectorStatus(
2239 this,
2240 connectorId,
2241 connectorStatus
2242 );
2243 await OCPPServiceUtils.sendAndSetConnectorStatus(
2244 this,
2245 connectorId,
12f26d4a
JB
2246 connectorBootStatus,
2247 evseId
4334db72
JB
2248 );
2249 }
c3b83130 2250 }
4334db72
JB
2251 }
2252 } else {
2253 for (const connectorId of this.connectors.keys()) {
2254 if (connectorId > 0) {
c3b83130
JB
2255 const connectorBootStatus = ChargingStationUtils.getBootConnectorStatus(
2256 this,
2257 connectorId,
4334db72 2258 this.getConnectorStatus(connectorId)
c3b83130
JB
2259 );
2260 await OCPPServiceUtils.sendAndSetConnectorStatus(this, connectorId, connectorBootStatus);
2261 }
2262 }
5ad8570f 2263 }
c9a4f9ea
JB
2264 if (this.stationInfo?.firmwareStatus === FirmwareStatus.Installing) {
2265 await this.ocppRequestService.requestHandler<
2266 FirmwareStatusNotificationRequest,
2267 FirmwareStatusNotificationResponse
2268 >(this, RequestCommand.FIRMWARE_STATUS_NOTIFICATION, {
2269 status: FirmwareStatus.Installed,
2270 });
2271 this.stationInfo.firmwareStatus = FirmwareStatus.Installed;
c9a4f9ea 2272 }
3637ca2c 2273
0a60c33c 2274 // Start the ATG
ac7f79af 2275 if (this.getAutomaticTransactionGeneratorConfiguration()?.enable === true) {
4f69be04 2276 this.startAutomaticTransactionGenerator();
fa7bccf4 2277 }
aa428a31 2278 this.wsConnectionRestarted === true && this.flushMessageBuffer();
fa7bccf4
JB
2279 }
2280
e7aeea18
JB
2281 private async stopMessageSequence(
2282 reason: StopTransactionReason = StopTransactionReason.NONE
2283 ): Promise<void> {
136c90ba 2284 // Stop WebSocket ping
c0560973 2285 this.stopWebSocketPing();
79411696 2286 // Stop heartbeat
c0560973 2287 this.stopHeartbeat();
fa7bccf4 2288 // Stop ongoing transactions
b20eb107 2289 if (this.automaticTransactionGenerator?.started === true) {
60ddad53
JB
2290 this.stopAutomaticTransactionGenerator();
2291 } else {
2292 await this.stopRunningTransactions(reason);
79411696 2293 }
039211f9
JB
2294 if (this.hasEvses) {
2295 for (const [evseId, evseStatus] of this.evses) {
2296 if (evseId > 0) {
2297 for (const [connectorId, connectorStatus] of evseStatus.connectors) {
2298 await this.ocppRequestService.requestHandler<
2299 StatusNotificationRequest,
2300 StatusNotificationResponse
2301 >(
2302 this,
2303 RequestCommand.STATUS_NOTIFICATION,
2304 OCPPServiceUtils.buildStatusNotificationRequest(
2305 this,
2306 connectorId,
12f26d4a
JB
2307 ConnectorStatusEnum.Unavailable,
2308 evseId
039211f9
JB
2309 )
2310 );
2311 delete connectorStatus?.status;
2312 }
2313 }
2314 }
2315 } else {
2316 for (const connectorId of this.connectors.keys()) {
2317 if (connectorId > 0) {
2318 await this.ocppRequestService.requestHandler<
2319 StatusNotificationRequest,
2320 StatusNotificationResponse
2321 >(
6e939d9e 2322 this,
039211f9
JB
2323 RequestCommand.STATUS_NOTIFICATION,
2324 OCPPServiceUtils.buildStatusNotificationRequest(
2325 this,
2326 connectorId,
2327 ConnectorStatusEnum.Unavailable
2328 )
2329 );
2330 delete this.getConnectorStatus(connectorId)?.status;
2331 }
45c0ae82
JB
2332 }
2333 }
79411696
JB
2334 }
2335
c0560973 2336 private startWebSocketPing(): void {
17ac262c
JB
2337 const webSocketPingInterval: number = ChargingStationConfigurationUtils.getConfigurationKey(
2338 this,
e7aeea18
JB
2339 StandardParametersKey.WebSocketPingInterval
2340 )
2341 ? Utils.convertToInt(
17ac262c
JB
2342 ChargingStationConfigurationUtils.getConfigurationKey(
2343 this,
2344 StandardParametersKey.WebSocketPingInterval
72092cfc 2345 )?.value
e7aeea18 2346 )
9cd3dfb0 2347 : 0;
ad2f27c3
JB
2348 if (webSocketPingInterval > 0 && !this.webSocketPingSetInterval) {
2349 this.webSocketPingSetInterval = setInterval(() => {
56eb297e 2350 if (this.isWebSocketConnectionOpened() === true) {
72092cfc 2351 this.wsConnection?.ping();
136c90ba
JB
2352 }
2353 }, webSocketPingInterval * 1000);
e7aeea18 2354 logger.info(
44eb6026
JB
2355 `${this.logPrefix()} WebSocket ping started every ${Utils.formatDurationSeconds(
2356 webSocketPingInterval
2357 )}`
e7aeea18 2358 );
ad2f27c3 2359 } else if (this.webSocketPingSetInterval) {
e7aeea18 2360 logger.info(
44eb6026
JB
2361 `${this.logPrefix()} WebSocket ping already started every ${Utils.formatDurationSeconds(
2362 webSocketPingInterval
2363 )}`
e7aeea18 2364 );
136c90ba 2365 } else {
e7aeea18 2366 logger.error(
8f953431 2367 `${this.logPrefix()} WebSocket ping interval set to ${webSocketPingInterval}, not starting the WebSocket ping`
e7aeea18 2368 );
136c90ba
JB
2369 }
2370 }
2371
c0560973 2372 private stopWebSocketPing(): void {
ad2f27c3
JB
2373 if (this.webSocketPingSetInterval) {
2374 clearInterval(this.webSocketPingSetInterval);
dfe81c8f 2375 delete this.webSocketPingSetInterval;
136c90ba
JB
2376 }
2377 }
2378
1f5df42a 2379 private getConfiguredSupervisionUrl(): URL {
d5c3df49 2380 let configuredSupervisionUrl: string;
72092cfc 2381 const supervisionUrls = this.stationInfo?.supervisionUrls ?? Configuration.getSupervisionUrls();
53ac516c 2382 if (Utils.isNotEmptyArray(supervisionUrls)) {
269de583 2383 let configuredSupervisionUrlIndex: number;
2dcfe98e 2384 switch (Configuration.getSupervisionUrlDistribution()) {
2dcfe98e 2385 case SupervisionUrlDistribution.RANDOM:
269de583 2386 configuredSupervisionUrlIndex = Math.floor(Utils.secureRandom() * supervisionUrls.length);
2dcfe98e 2387 break;
a52a6446 2388 case SupervisionUrlDistribution.ROUND_ROBIN:
c72f6634 2389 case SupervisionUrlDistribution.CHARGING_STATION_AFFINITY:
2dcfe98e 2390 default:
a52a6446
JB
2391 Object.values(SupervisionUrlDistribution).includes(
2392 Configuration.getSupervisionUrlDistribution()
2393 ) === false &&
2394 logger.error(
2395 `${this.logPrefix()} Unknown supervision url distribution '${Configuration.getSupervisionUrlDistribution()}' from values '${SupervisionUrlDistribution.toString()}', defaulting to ${
2396 SupervisionUrlDistribution.CHARGING_STATION_AFFINITY
2397 }`
2398 );
269de583 2399 configuredSupervisionUrlIndex = (this.index - 1) % supervisionUrls.length;
2dcfe98e 2400 break;
c0560973 2401 }
d5c3df49
JB
2402 configuredSupervisionUrl = supervisionUrls[configuredSupervisionUrlIndex];
2403 } else {
2404 configuredSupervisionUrl = supervisionUrls as string;
2405 }
2406 if (Utils.isNotEmptyString(configuredSupervisionUrl)) {
2407 return new URL(configuredSupervisionUrl);
c0560973 2408 }
49c508b0 2409 const errorMsg = 'No supervision url(s) configured';
7f77d16f
JB
2410 logger.error(`${this.logPrefix()} ${errorMsg}`);
2411 throw new BaseError(`${errorMsg}`);
136c90ba
JB
2412 }
2413
c0560973 2414 private stopHeartbeat(): void {
ad2f27c3
JB
2415 if (this.heartbeatSetInterval) {
2416 clearInterval(this.heartbeatSetInterval);
dfe81c8f 2417 delete this.heartbeatSetInterval;
7dde0b73 2418 }
5ad8570f
JB
2419 }
2420
55516218 2421 private terminateWSConnection(): void {
56eb297e 2422 if (this.isWebSocketConnectionOpened() === true) {
72092cfc 2423 this.wsConnection?.terminate();
55516218
JB
2424 this.wsConnection = null;
2425 }
2426 }
2427
c72f6634 2428 private getReconnectExponentialDelay(): boolean {
a14885a3 2429 return this.stationInfo?.reconnectExponentialDelay ?? false;
5ad8570f
JB
2430 }
2431
aa428a31 2432 private async reconnect(): Promise<void> {
7874b0b1
JB
2433 // Stop WebSocket ping
2434 this.stopWebSocketPing();
136c90ba 2435 // Stop heartbeat
c0560973 2436 this.stopHeartbeat();
5ad8570f 2437 // Stop the ATG if needed
ac7f79af 2438 if (this.getAutomaticTransactionGeneratorConfiguration().stopOnConnectionFailure === true) {
fa7bccf4 2439 this.stopAutomaticTransactionGenerator();
ad2f27c3 2440 }
e7aeea18
JB
2441 if (
2442 this.autoReconnectRetryCount < this.getAutoReconnectMaxRetries() ||
2443 this.getAutoReconnectMaxRetries() === -1
2444 ) {
ad2f27c3 2445 this.autoReconnectRetryCount++;
e7aeea18
JB
2446 const reconnectDelay = this.getReconnectExponentialDelay()
2447 ? Utils.exponentialDelay(this.autoReconnectRetryCount)
2448 : this.getConnectionTimeout() * 1000;
1e080116
JB
2449 const reconnectDelayWithdraw = 1000;
2450 const reconnectTimeout =
2451 reconnectDelay && reconnectDelay - reconnectDelayWithdraw > 0
2452 ? reconnectDelay - reconnectDelayWithdraw
2453 : 0;
e7aeea18 2454 logger.error(
d56ea27c 2455 `${this.logPrefix()} WebSocket connection retry in ${Utils.roundTo(
e7aeea18
JB
2456 reconnectDelay,
2457 2
2458 )}ms, timeout ${reconnectTimeout}ms`
2459 );
032d6efc 2460 await Utils.sleep(reconnectDelay);
e7aeea18 2461 logger.error(
44eb6026 2462 `${this.logPrefix()} WebSocket connection retry #${this.autoReconnectRetryCount.toString()}`
e7aeea18
JB
2463 );
2464 this.openWSConnection(
59b6ed8d 2465 {
abe9e9dd 2466 ...(this.stationInfo?.wsOptions ?? {}),
59b6ed8d
JB
2467 handshakeTimeout: reconnectTimeout,
2468 },
1e080116 2469 { closeOpened: true }
e7aeea18 2470 );
265e4266 2471 this.wsConnectionRestarted = true;
c0560973 2472 } else if (this.getAutoReconnectMaxRetries() !== -1) {
e7aeea18 2473 logger.error(
d56ea27c 2474 `${this.logPrefix()} WebSocket connection retries failure: maximum retries reached (${
e7aeea18 2475 this.autoReconnectRetryCount
d56ea27c 2476 }) or retries disabled (${this.getAutoReconnectMaxRetries()})`
e7aeea18 2477 );
5ad8570f
JB
2478 }
2479 }
7dde0b73 2480}