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