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