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