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