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