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