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