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