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