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