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