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