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