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