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