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