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