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