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