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