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