fix: find the first free indexes at adding charging stations
[e-mobility-charging-stations-simulator.git] / src / charging-station / Helpers.ts
CommitLineData
66a7748d
JB
1import { createHash, randomBytes } from 'node:crypto'
2import type { EventEmitter } from 'node:events'
3import { basename, dirname, join } from 'node:path'
4import { env } from 'node:process'
5import { fileURLToPath } from 'node:url'
8114d10e 6
66a7748d 7import chalk from 'chalk'
f924d466 8import {
f1e3871b 9 type Interval,
f924d466 10 addDays,
f924d466
JB
11 addSeconds,
12 addWeeks,
497588ef 13 differenceInDays,
d476bc1b 14 differenceInSeconds,
497588ef 15 differenceInWeeks,
f924d466
JB
16 isAfter,
17 isBefore,
0bd926c1 18 isDate,
90aceaf6 19 isPast,
252a7d22 20 isWithinInterval,
66a7748d
JB
21 toDate
22} from 'date-fns'
23import { maxTime } from 'date-fns/constants'
8114d10e 24
66a7748d
JB
25import type { ChargingStation } from './ChargingStation.js'
26import { getConfigurationKey } from './ConfigurationKeyUtils.js'
27import { BaseError } from '../exception/index.js'
981ebfbe 28import {
492cf6ab 29 AmpereUnits,
04b1261c 30 AvailabilityType,
268a74bb
JB
31 type BootNotificationRequest,
32 BootReasonEnumType,
15068be9 33 type ChargingProfile,
268a74bb 34 ChargingProfileKindType,
15068be9
JB
35 ChargingRateUnitType,
36 type ChargingSchedulePeriod,
73edcc94 37 type ChargingStationConfiguration,
268a74bb 38 type ChargingStationInfo,
36b73d95 39 type ChargingStationOptions,
268a74bb 40 type ChargingStationTemplate,
66a7748d 41 type ChargingStationWorkerMessageEvents,
dd08d43d 42 ConnectorPhaseRotation,
a78ef5ed 43 type ConnectorStatus,
c3b83130 44 ConnectorStatusEnum,
268a74bb 45 CurrentType,
ae25f265 46 type EvseTemplate,
268a74bb
JB
47 type OCPP16BootNotificationRequest,
48 type OCPP20BootNotificationRequest,
49 OCPPVersion,
50 RecurrencyKindType,
90aceaf6
JB
51 type Reservation,
52 ReservationTerminationReason,
d8093be1 53 StandardParametersKey,
66a7748d
JB
54 type SupportedFeatureProfiles,
55 Voltage
56} from '../types/index.js'
9bf0ef23
JB
57import {
58 ACElectricUtils,
59 Constants,
60 DCElectricUtils,
40615072 61 clone,
b85cef4c 62 convertToDate,
9bf0ef23 63 convertToInt,
80c58041 64 isArraySorted,
9bf0ef23
JB
65 isEmptyObject,
66 isEmptyString,
67 isNotEmptyArray,
68 isNotEmptyString,
5dc7c990 69 isValidDate,
9bf0ef23 70 logger,
66a7748d
JB
71 secureRandom
72} from '../utils/index.js'
17ac262c 73
66a7748d 74const moduleName = 'Helpers'
91a4f151 75
fba11dc6
JB
76export const getChargingStationId = (
77 index: number,
66a7748d 78 stationTemplate: ChargingStationTemplate | undefined
fba11dc6 79): string => {
a807045b 80 if (stationTemplate == null) {
66a7748d 81 return "Unknown 'chargingStationId'"
c1f16afd 82 }
fba11dc6 83 // In case of multiple instances: add instance index to charging station id
66a7748d 84 const instanceIndex = env.CF_INSTANCE_INDEX ?? 0
5199f9fd 85 const idSuffix = stationTemplate.nameSuffix ?? ''
66a7748d 86 const idStr = `000000000${index.toString()}`
5199f9fd 87 return stationTemplate.fixedName === true
fba11dc6 88 ? stationTemplate.baseName
f1bd9d19 89 : `${stationTemplate.baseName}-${instanceIndex.toString()}${idStr.substring(
66a7748d
JB
90 idStr.length - 4
91 )}${idSuffix}`
92}
fba11dc6 93
90aceaf6 94export const hasReservationExpired = (reservation: Reservation): boolean => {
66a7748d
JB
95 return isPast(reservation.expiryDate)
96}
90aceaf6
JB
97
98export const removeExpiredReservations = async (
66a7748d 99 chargingStation: ChargingStation
90aceaf6
JB
100): Promise<void> => {
101 if (chargingStation.hasEvses) {
102 for (const evseStatus of chargingStation.evses.values()) {
103 for (const connectorStatus of evseStatus.connectors.values()) {
66a7748d
JB
104 if (
105 connectorStatus.reservation != null &&
106 hasReservationExpired(connectorStatus.reservation)
107 ) {
90aceaf6
JB
108 await chargingStation.removeReservation(
109 connectorStatus.reservation,
66a7748d
JB
110 ReservationTerminationReason.EXPIRED
111 )
90aceaf6
JB
112 }
113 }
114 }
115 } else {
116 for (const connectorStatus of chargingStation.connectors.values()) {
66a7748d
JB
117 if (
118 connectorStatus.reservation != null &&
119 hasReservationExpired(connectorStatus.reservation)
120 ) {
90aceaf6
JB
121 await chargingStation.removeReservation(
122 connectorStatus.reservation,
66a7748d
JB
123 ReservationTerminationReason.EXPIRED
124 )
90aceaf6
JB
125 }
126 }
127 }
66a7748d 128}
90aceaf6
JB
129
130export const getNumberOfReservableConnectors = (
66a7748d 131 connectors: Map<number, ConnectorStatus>
90aceaf6 132): number => {
66a7748d 133 let numberOfReservableConnectors = 0
fba11dc6
JB
134 for (const [connectorId, connectorStatus] of connectors) {
135 if (connectorId === 0) {
66a7748d 136 continue
3fa7f799 137 }
fba11dc6 138 if (connectorStatus.status === ConnectorStatusEnum.Available) {
66a7748d 139 ++numberOfReservableConnectors
1bf29f5b 140 }
1bf29f5b 141 }
66a7748d
JB
142 return numberOfReservableConnectors
143}
fba11dc6
JB
144
145export const getHashId = (index: number, stationTemplate: ChargingStationTemplate): string => {
146 const chargingStationInfo = {
147 chargePointModel: stationTemplate.chargePointModel,
148 chargePointVendor: stationTemplate.chargePointVendor,
d760a0a6 149 ...(stationTemplate.chargeBoxSerialNumberPrefix != null && {
66a7748d 150 chargeBoxSerialNumber: stationTemplate.chargeBoxSerialNumberPrefix
fba11dc6 151 }),
d760a0a6 152 ...(stationTemplate.chargePointSerialNumberPrefix != null && {
66a7748d 153 chargePointSerialNumber: stationTemplate.chargePointSerialNumberPrefix
fba11dc6 154 }),
d760a0a6 155 ...(stationTemplate.meterSerialNumberPrefix != null && {
66a7748d 156 meterSerialNumber: stationTemplate.meterSerialNumberPrefix
fba11dc6 157 }),
d760a0a6 158 ...(stationTemplate.meterType != null && {
66a7748d
JB
159 meterType: stationTemplate.meterType
160 })
161 }
fba11dc6
JB
162 return createHash(Constants.DEFAULT_HASH_ALGORITHM)
163 .update(`${JSON.stringify(chargingStationInfo)}${getChargingStationId(index, stationTemplate)}`)
66a7748d
JB
164 .digest('hex')
165}
fba11dc6
JB
166
167export const checkChargingStation = (
168 chargingStation: ChargingStation,
66a7748d 169 logPrefix: string
fba11dc6 170): boolean => {
66a7748d
JB
171 if (!chargingStation.started && !chargingStation.starting) {
172 logger.warn(`${logPrefix} charging station is stopped, cannot proceed`)
173 return false
fba11dc6 174 }
66a7748d
JB
175 return true
176}
fba11dc6
JB
177
178export const getPhaseRotationValue = (
179 connectorId: number,
66a7748d 180 numberOfPhases: number
fba11dc6
JB
181): string | undefined => {
182 // AC/DC
183 if (connectorId === 0 && numberOfPhases === 0) {
66a7748d 184 return `${connectorId}.${ConnectorPhaseRotation.RST}`
fba11dc6 185 } else if (connectorId > 0 && numberOfPhases === 0) {
66a7748d 186 return `${connectorId}.${ConnectorPhaseRotation.NotApplicable}`
fba11dc6 187 // AC
d181b12b 188 } else if (connectorId >= 0 && numberOfPhases === 1) {
66a7748d 189 return `${connectorId}.${ConnectorPhaseRotation.NotApplicable}`
d181b12b 190 } else if (connectorId >= 0 && numberOfPhases === 3) {
66a7748d 191 return `${connectorId}.${ConnectorPhaseRotation.RST}`
fba11dc6 192 }
66a7748d 193}
fba11dc6 194
5199f9fd 195export const getMaxNumberOfEvses = (evses: Record<string, EvseTemplate> | undefined): number => {
66a7748d
JB
196 if (evses == null) {
197 return -1
fba11dc6 198 }
66a7748d
JB
199 return Object.keys(evses).length
200}
fba11dc6 201
5199f9fd
JB
202const getMaxNumberOfConnectors = (
203 connectors: Record<string, ConnectorStatus> | undefined
204): number => {
66a7748d
JB
205 if (connectors == null) {
206 return -1
fba11dc6 207 }
66a7748d
JB
208 return Object.keys(connectors).length
209}
fba11dc6
JB
210
211export const getBootConnectorStatus = (
212 chargingStation: ChargingStation,
213 connectorId: number,
66a7748d 214 connectorStatus: ConnectorStatus
fba11dc6 215): ConnectorStatusEnum => {
66a7748d 216 let connectorBootStatus: ConnectorStatusEnum
fba11dc6 217 if (
5199f9fd 218 connectorStatus.status == null &&
66a7748d
JB
219 (!chargingStation.isChargingStationAvailable() ||
220 !chargingStation.isConnectorAvailable(connectorId))
fba11dc6 221 ) {
66a7748d 222 connectorBootStatus = ConnectorStatusEnum.Unavailable
5199f9fd 223 } else if (connectorStatus.status == null && connectorStatus.bootStatus != null) {
fba11dc6 224 // Set boot status in template at startup
5199f9fd
JB
225 connectorBootStatus = connectorStatus.bootStatus
226 } else if (connectorStatus.status != null) {
fba11dc6 227 // Set previous status at startup
5199f9fd 228 connectorBootStatus = connectorStatus.status
fba11dc6
JB
229 } else {
230 // Set default status
66a7748d 231 connectorBootStatus = ConnectorStatusEnum.Available
fba11dc6 232 }
66a7748d
JB
233 return connectorBootStatus
234}
fba11dc6
JB
235
236export const checkTemplate = (
5199f9fd 237 stationTemplate: ChargingStationTemplate | undefined,
fba11dc6 238 logPrefix: string,
66a7748d 239 templateFile: string
fba11dc6 240): void => {
66a7748d
JB
241 if (stationTemplate == null) {
242 const errorMsg = `Failed to read charging station template file ${templateFile}`
243 logger.error(`${logPrefix} ${errorMsg}`)
244 throw new BaseError(errorMsg)
fba11dc6
JB
245 }
246 if (isEmptyObject(stationTemplate)) {
66a7748d
JB
247 const errorMsg = `Empty charging station information from template file ${templateFile}`
248 logger.error(`${logPrefix} ${errorMsg}`)
249 throw new BaseError(errorMsg)
fba11dc6 250 }
be9f397b 251 if (stationTemplate.idTagsFile == null || isEmptyString(stationTemplate.idTagsFile)) {
fba11dc6 252 logger.warn(
66a7748d
JB
253 `${logPrefix} Missing id tags file in template file ${templateFile}. That can lead to issues with the Automatic Transaction Generator`
254 )
c3b83130 255 }
66a7748d 256}
fba11dc6 257
73edcc94
JB
258export const checkConfiguration = (
259 stationConfiguration: ChargingStationConfiguration | undefined,
260 logPrefix: string,
66a7748d 261 configurationFile: string
73edcc94 262): void => {
a807045b 263 if (stationConfiguration == null) {
66a7748d
JB
264 const errorMsg = `Failed to read charging station configuration file ${configurationFile}`
265 logger.error(`${logPrefix} ${errorMsg}`)
266 throw new BaseError(errorMsg)
73edcc94 267 }
a807045b 268 if (isEmptyObject(stationConfiguration)) {
66a7748d
JB
269 const errorMsg = `Empty charging station configuration from file ${configurationFile}`
270 logger.error(`${logPrefix} ${errorMsg}`)
271 throw new BaseError(errorMsg)
73edcc94 272 }
66a7748d 273}
73edcc94 274
fba11dc6
JB
275export const checkConnectorsConfiguration = (
276 stationTemplate: ChargingStationTemplate,
277 logPrefix: string,
66a7748d 278 templateFile: string
fba11dc6 279): {
66a7748d
JB
280 configuredMaxConnectors: number
281 templateMaxConnectors: number
282 templateMaxAvailableConnectors: number
fba11dc6 283} => {
66a7748d
JB
284 const configuredMaxConnectors = getConfiguredMaxNumberOfConnectors(stationTemplate)
285 checkConfiguredMaxConnectors(configuredMaxConnectors, logPrefix, templateFile)
5199f9fd 286 const templateMaxConnectors = getMaxNumberOfConnectors(stationTemplate.Connectors)
66a7748d
JB
287 checkTemplateMaxConnectors(templateMaxConnectors, logPrefix, templateFile)
288 const templateMaxAvailableConnectors =
289 stationTemplate.Connectors?.[0] != null ? templateMaxConnectors - 1 : templateMaxConnectors
fba11dc6
JB
290 if (
291 configuredMaxConnectors > templateMaxAvailableConnectors &&
5199f9fd 292 stationTemplate.randomConnectors !== true
8a133cc8 293 ) {
fba11dc6 294 logger.warn(
66a7748d
JB
295 `${logPrefix} Number of connectors exceeds the number of connector configurations in template ${templateFile}, forcing random connector configurations affectation`
296 )
297 stationTemplate.randomConnectors = true
fba11dc6 298 }
66a7748d
JB
299 return { configuredMaxConnectors, templateMaxConnectors, templateMaxAvailableConnectors }
300}
fba11dc6
JB
301
302export const checkStationInfoConnectorStatus = (
303 connectorId: number,
304 connectorStatus: ConnectorStatus,
305 logPrefix: string,
66a7748d 306 templateFile: string
fba11dc6 307): void => {
5199f9fd 308 if (connectorStatus.status != null) {
fba11dc6 309 logger.warn(
66a7748d
JB
310 `${logPrefix} Charging station information from template ${templateFile} with connector id ${connectorId} status configuration defined, undefine it`
311 )
312 delete connectorStatus.status
fba11dc6 313 }
66a7748d 314}
fba11dc6
JB
315
316export const buildConnectorsMap = (
317 connectors: Record<string, ConnectorStatus>,
318 logPrefix: string,
66a7748d 319 templateFile: string
fba11dc6 320): Map<number, ConnectorStatus> => {
66a7748d 321 const connectorsMap = new Map<number, ConnectorStatus>()
fba11dc6
JB
322 if (getMaxNumberOfConnectors(connectors) > 0) {
323 for (const connector in connectors) {
66a7748d
JB
324 const connectorStatus = connectors[connector]
325 const connectorId = convertToInt(connector)
326 checkStationInfoConnectorStatus(connectorId, connectorStatus, logPrefix, templateFile)
40615072 327 connectorsMap.set(connectorId, clone<ConnectorStatus>(connectorStatus))
fa7bccf4 328 }
fba11dc6
JB
329 } else {
330 logger.warn(
66a7748d
JB
331 `${logPrefix} Charging station information from template ${templateFile} with no connectors, cannot build connectors map`
332 )
fa7bccf4 333 }
66a7748d
JB
334 return connectorsMap
335}
fa7bccf4 336
36b73d95
JB
337export const setChargingStationOptions = (
338 stationInfo: ChargingStationInfo,
339 options?: ChargingStationOptions
340): ChargingStationInfo => {
341 if (options?.persistentConfiguration != null) {
c2be11fd 342 stationInfo.stationInfoPersistentConfiguration = options.persistentConfiguration
36b73d95
JB
343 stationInfo.ocppPersistentConfiguration = options.persistentConfiguration
344 stationInfo.automaticTransactionGeneratorPersistentConfiguration =
345 options.persistentConfiguration
346 }
1253d60c
JB
347 if (options?.autoStart != null) {
348 stationInfo.autoStart = options.autoStart
349 }
36b73d95
JB
350 if (options?.autoRegister != null) {
351 stationInfo.autoRegister = options.autoRegister
352 }
353 if (options?.enableStatistics != null) {
354 stationInfo.enableStatistics = options.enableStatistics
355 }
356 if (options?.ocppStrictCompliance != null) {
357 stationInfo.ocppStrictCompliance = options.ocppStrictCompliance
358 }
359 if (options?.stopTransactionsOnStopped != null) {
360 stationInfo.stopTransactionsOnStopped = options.stopTransactionsOnStopped
361 }
362 return stationInfo
363}
364
fba11dc6
JB
365export const initializeConnectorsMapStatus = (
366 connectors: Map<number, ConnectorStatus>,
66a7748d 367 logPrefix: string
fba11dc6
JB
368): void => {
369 for (const connectorId of connectors.keys()) {
370 if (connectorId > 0 && connectors.get(connectorId)?.transactionStarted === true) {
04b1261c 371 logger.warn(
a223d9be
JB
372 `${logPrefix} Connector id ${connectorId} at initialization has a transaction started with id ${
373 connectors.get(connectorId)?.transactionId
374 }`
66a7748d 375 )
04b1261c 376 }
fba11dc6 377 if (connectorId === 0) {
66a7748d
JB
378 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
379 connectors.get(connectorId)!.availability = AvailabilityType.Operative
300418e9 380 if (connectors.get(connectorId)?.chargingProfiles == null) {
66a7748d
JB
381 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
382 connectors.get(connectorId)!.chargingProfiles = []
04b1261c 383 }
be9f397b 384 } else if (connectorId > 0 && connectors.get(connectorId)?.transactionStarted == null) {
66a7748d
JB
385 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
386 initializeConnectorStatus(connectors.get(connectorId)!)
04b1261c
JB
387 }
388 }
66a7748d 389}
fba11dc6 390
f938317f
JB
391export const resetConnectorStatus = (connectorStatus: ConnectorStatus | undefined): void => {
392 if (connectorStatus == null) {
393 return
394 }
86f51b96 395 connectorStatus.chargingProfiles =
66a7748d 396 connectorStatus.transactionId != null && isNotEmptyArray(connectorStatus.chargingProfiles)
5dc7c990 397 ? connectorStatus.chargingProfiles.filter(
a974c8e4 398 chargingProfile => chargingProfile.transactionId !== connectorStatus.transactionId
66a7748d
JB
399 )
400 : []
401 connectorStatus.idTagLocalAuthorized = false
402 connectorStatus.idTagAuthorized = false
403 connectorStatus.transactionRemoteStarted = false
404 connectorStatus.transactionStarted = false
5199f9fd
JB
405 delete connectorStatus.transactionStart
406 delete connectorStatus.transactionId
407 delete connectorStatus.localAuthorizeIdTag
408 delete connectorStatus.authorizeIdTag
409 delete connectorStatus.transactionIdTag
66a7748d 410 connectorStatus.transactionEnergyActiveImportRegisterValue = 0
5199f9fd 411 delete connectorStatus.transactionBeginMeterValue
66a7748d 412}
fba11dc6
JB
413
414export const createBootNotificationRequest = (
415 stationInfo: ChargingStationInfo,
66a7748d 416 bootReason: BootReasonEnumType = BootReasonEnumType.PowerUp
2466918c
JB
417): BootNotificationRequest | undefined => {
418 const ocppVersion = stationInfo.ocppVersion
fba11dc6
JB
419 switch (ocppVersion) {
420 case OCPPVersion.VERSION_16:
421 return {
422 chargePointModel: stationInfo.chargePointModel,
423 chargePointVendor: stationInfo.chargePointVendor,
d760a0a6 424 ...(stationInfo.chargeBoxSerialNumber != null && {
66a7748d 425 chargeBoxSerialNumber: stationInfo.chargeBoxSerialNumber
fba11dc6 426 }),
d760a0a6 427 ...(stationInfo.chargePointSerialNumber != null && {
66a7748d 428 chargePointSerialNumber: stationInfo.chargePointSerialNumber
fba11dc6 429 }),
d760a0a6 430 ...(stationInfo.firmwareVersion != null && {
66a7748d 431 firmwareVersion: stationInfo.firmwareVersion
fba11dc6 432 }),
d760a0a6
JB
433 ...(stationInfo.iccid != null && { iccid: stationInfo.iccid }),
434 ...(stationInfo.imsi != null && { imsi: stationInfo.imsi }),
435 ...(stationInfo.meterSerialNumber != null && {
66a7748d 436 meterSerialNumber: stationInfo.meterSerialNumber
fba11dc6 437 }),
d760a0a6 438 ...(stationInfo.meterType != null && {
66a7748d
JB
439 meterType: stationInfo.meterType
440 })
441 } satisfies OCPP16BootNotificationRequest
fba11dc6
JB
442 case OCPPVersion.VERSION_20:
443 case OCPPVersion.VERSION_201:
444 return {
445 reason: bootReason,
446 chargingStation: {
447 model: stationInfo.chargePointModel,
448 vendorName: stationInfo.chargePointVendor,
d760a0a6 449 ...(stationInfo.firmwareVersion != null && {
66a7748d 450 firmwareVersion: stationInfo.firmwareVersion
d270cc87 451 }),
d760a0a6 452 ...(stationInfo.chargeBoxSerialNumber != null && {
66a7748d 453 serialNumber: stationInfo.chargeBoxSerialNumber
d270cc87 454 }),
d760a0a6 455 ...((stationInfo.iccid != null || stationInfo.imsi != null) && {
fba11dc6 456 modem: {
d760a0a6
JB
457 ...(stationInfo.iccid != null && { iccid: stationInfo.iccid }),
458 ...(stationInfo.imsi != null && { imsi: stationInfo.imsi })
66a7748d
JB
459 }
460 })
461 }
462 } satisfies OCPP20BootNotificationRequest
fba11dc6 463 }
66a7748d 464}
fba11dc6
JB
465
466export const warnTemplateKeysDeprecation = (
467 stationTemplate: ChargingStationTemplate,
468 logPrefix: string,
66a7748d
JB
469 templateFile: string
470): void => {
471 const templateKeys: Array<{ deprecatedKey: string, key?: string }> = [
e4c6cf05
JB
472 { deprecatedKey: 'supervisionUrl', key: 'supervisionUrls' },
473 { deprecatedKey: 'authorizationFile', key: 'idTagsFile' },
474 { deprecatedKey: 'payloadSchemaValidation', key: 'ocppStrictCompliance' },
66a7748d
JB
475 { deprecatedKey: 'mustAuthorizeAtRemoteStart', key: 'remoteAuthorization' }
476 ]
fba11dc6
JB
477 for (const templateKey of templateKeys) {
478 warnDeprecatedTemplateKey(
479 stationTemplate,
480 templateKey.deprecatedKey,
481 logPrefix,
482 templateFile,
d760a0a6 483 templateKey.key != null ? `Use '${templateKey.key}' instead` : undefined
66a7748d
JB
484 )
485 convertDeprecatedTemplateKey(stationTemplate, templateKey.deprecatedKey, templateKey.key)
fba11dc6 486 }
66a7748d 487}
fba11dc6
JB
488
489export const stationTemplateToStationInfo = (
66a7748d 490 stationTemplate: ChargingStationTemplate
fba11dc6 491): ChargingStationInfo => {
40615072 492 stationTemplate = clone<ChargingStationTemplate>(stationTemplate)
66a7748d
JB
493 delete stationTemplate.power
494 delete stationTemplate.powerUnit
495 delete stationTemplate.Connectors
496 delete stationTemplate.Evses
497 delete stationTemplate.Configuration
498 delete stationTemplate.AutomaticTransactionGenerator
499 delete stationTemplate.chargeBoxSerialNumberPrefix
500 delete stationTemplate.chargePointSerialNumberPrefix
501 delete stationTemplate.meterSerialNumberPrefix
502 return stationTemplate as ChargingStationInfo
503}
fba11dc6
JB
504
505export const createSerialNumber = (
506 stationTemplate: ChargingStationTemplate,
507 stationInfo: ChargingStationInfo,
7f3decca 508 params?: {
66a7748d
JB
509 randomSerialNumberUpperCase?: boolean
510 randomSerialNumber?: boolean
511 }
fba11dc6 512): void => {
66a7748d
JB
513 params = { ...{ randomSerialNumberUpperCase: true, randomSerialNumber: true }, ...params }
514 const serialNumberSuffix =
5199f9fd 515 params.randomSerialNumber === true
66a7748d
JB
516 ? getRandomSerialNumberSuffix({
517 upperCase: params.randomSerialNumberUpperCase
fba11dc6 518 })
66a7748d 519 : ''
5199f9fd 520 isNotEmptyString(stationTemplate.chargePointSerialNumberPrefix) &&
66a7748d 521 (stationInfo.chargePointSerialNumber = `${stationTemplate.chargePointSerialNumberPrefix}${serialNumberSuffix}`)
5199f9fd 522 isNotEmptyString(stationTemplate.chargeBoxSerialNumberPrefix) &&
66a7748d 523 (stationInfo.chargeBoxSerialNumber = `${stationTemplate.chargeBoxSerialNumberPrefix}${serialNumberSuffix}`)
5199f9fd 524 isNotEmptyString(stationTemplate.meterSerialNumberPrefix) &&
66a7748d
JB
525 (stationInfo.meterSerialNumber = `${stationTemplate.meterSerialNumberPrefix}${serialNumberSuffix}`)
526}
fba11dc6
JB
527
528export const propagateSerialNumber = (
5199f9fd
JB
529 stationTemplate: ChargingStationTemplate | undefined,
530 stationInfoSrc: ChargingStationInfo | undefined,
66a7748d
JB
531 stationInfoDst: ChargingStationInfo
532): void => {
533 if (stationInfoSrc == null || stationTemplate == null) {
fba11dc6 534 throw new BaseError(
66a7748d
JB
535 'Missing charging station template or existing configuration to propagate serial number'
536 )
17ac262c 537 }
5199f9fd
JB
538 stationTemplate.chargePointSerialNumberPrefix != null &&
539 stationInfoSrc.chargePointSerialNumber != null
fba11dc6 540 ? (stationInfoDst.chargePointSerialNumber = stationInfoSrc.chargePointSerialNumber)
5199f9fd 541 : stationInfoDst.chargePointSerialNumber != null &&
66a7748d 542 delete stationInfoDst.chargePointSerialNumber
5199f9fd
JB
543 stationTemplate.chargeBoxSerialNumberPrefix != null &&
544 stationInfoSrc.chargeBoxSerialNumber != null
fba11dc6 545 ? (stationInfoDst.chargeBoxSerialNumber = stationInfoSrc.chargeBoxSerialNumber)
5199f9fd
JB
546 : stationInfoDst.chargeBoxSerialNumber != null && delete stationInfoDst.chargeBoxSerialNumber
547 stationTemplate.meterSerialNumberPrefix != null && stationInfoSrc.meterSerialNumber != null
fba11dc6 548 ? (stationInfoDst.meterSerialNumber = stationInfoSrc.meterSerialNumber)
5199f9fd 549 : stationInfoDst.meterSerialNumber != null && delete stationInfoDst.meterSerialNumber
66a7748d 550}
fba11dc6 551
d8093be1
JB
552export const hasFeatureProfile = (
553 chargingStation: ChargingStation,
66a7748d 554 featureProfile: SupportedFeatureProfiles
d8093be1
JB
555): boolean | undefined => {
556 return getConfigurationKey(
557 chargingStation,
66a7748d
JB
558 StandardParametersKey.SupportedFeatureProfiles
559 )?.value?.includes(featureProfile)
560}
d8093be1 561
fba11dc6 562export const getAmperageLimitationUnitDivider = (stationInfo: ChargingStationInfo): number => {
66a7748d 563 let unitDivider = 1
fba11dc6
JB
564 switch (stationInfo.amperageLimitationUnit) {
565 case AmpereUnits.DECI_AMPERE:
66a7748d
JB
566 unitDivider = 10
567 break
fba11dc6 568 case AmpereUnits.CENTI_AMPERE:
66a7748d
JB
569 unitDivider = 100
570 break
fba11dc6 571 case AmpereUnits.MILLI_AMPERE:
66a7748d
JB
572 unitDivider = 1000
573 break
fba11dc6 574 }
66a7748d
JB
575 return unitDivider
576}
fba11dc6 577
6fc0c6f3
JB
578/**
579 * Gets the connector cloned charging profiles applying a power limitation
21ee4dc2 580 * and sorted by connector id descending then stack level descending
6fc0c6f3
JB
581 *
582 * @param chargingStation -
583 * @param connectorId -
584 * @returns connector charging profiles array
585 */
586export const getConnectorChargingProfiles = (
587 chargingStation: ChargingStation,
66a7748d
JB
588 connectorId: number
589): ChargingProfile[] => {
40615072 590 return clone<ChargingProfile[]>(
21ee4dc2 591 (chargingStation.getConnectorStatus(connectorId)?.chargingProfiles ?? [])
6fc0c6f3
JB
592 .sort((a, b) => b.stackLevel - a.stackLevel)
593 .concat(
21ee4dc2 594 (chargingStation.getConnectorStatus(0)?.chargingProfiles ?? []).sort(
66a7748d
JB
595 (a, b) => b.stackLevel - a.stackLevel
596 )
597 )
598 )
599}
6fc0c6f3 600
fba11dc6
JB
601export const getChargingStationConnectorChargingProfilesPowerLimit = (
602 chargingStation: ChargingStation,
66a7748d 603 connectorId: number
fba11dc6 604): number | undefined => {
66a7748d 605 let limit: number | undefined, chargingProfile: ChargingProfile | undefined
6fc0c6f3 606 // Get charging profiles sorted by connector id then stack level
66a7748d 607 const chargingProfiles = getConnectorChargingProfiles(chargingStation, connectorId)
fba11dc6 608 if (isNotEmptyArray(chargingProfiles)) {
a71d4e70
JB
609 const result = getLimitFromChargingProfiles(
610 chargingStation,
611 connectorId,
612 chargingProfiles,
66a7748d
JB
613 chargingStation.logPrefix()
614 )
be9f397b 615 if (result != null) {
5199f9fd
JB
616 limit = result.limit
617 chargingProfile = result.chargingProfile
5398cecf 618 switch (chargingStation.stationInfo?.currentOutType) {
fba11dc6
JB
619 case CurrentType.AC:
620 limit =
5199f9fd 621 chargingProfile.chargingSchedule.chargingRateUnit === ChargingRateUnitType.WATT
fba11dc6
JB
622 ? limit
623 : ACElectricUtils.powerTotal(
66a7748d
JB
624 chargingStation.getNumberOfPhases(),
625 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
626 chargingStation.stationInfo.voltageOut!,
be9f397b 627 limit
66a7748d
JB
628 )
629 break
fba11dc6
JB
630 case CurrentType.DC:
631 limit =
5199f9fd 632 chargingProfile.chargingSchedule.chargingRateUnit === ChargingRateUnitType.WATT
fba11dc6 633 ? limit
66a7748d 634 : // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
be9f397b 635 DCElectricUtils.power(chargingStation.stationInfo.voltageOut!, limit)
fba11dc6
JB
636 }
637 const connectorMaximumPower =
66a7748d 638 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
5199f9fd 639 chargingStation.stationInfo!.maximumPower! / chargingStation.powerDivider!
be9f397b 640 if (limit > connectorMaximumPower) {
fba11dc6 641 logger.error(
5199f9fd
JB
642 `${chargingStation.logPrefix()} ${moduleName}.getChargingStationConnectorChargingProfilesPowerLimit: Charging profile id ${
643 chargingProfile.chargingProfileId
644 } limit ${limit} is greater than connector id ${connectorId} maximum ${connectorMaximumPower}: %j`,
66a7748d
JB
645 result
646 )
647 limit = connectorMaximumPower
15068be9
JB
648 }
649 }
15068be9 650 }
66a7748d
JB
651 return limit
652}
fba11dc6
JB
653
654export const getDefaultVoltageOut = (
655 currentType: CurrentType,
656 logPrefix: string,
66a7748d 657 templateFile: string
fba11dc6 658): Voltage => {
66a7748d
JB
659 const errorMsg = `Unknown ${currentType} currentOutType in template file ${templateFile}, cannot define default voltage out`
660 let defaultVoltageOut: number
fba11dc6
JB
661 switch (currentType) {
662 case CurrentType.AC:
66a7748d
JB
663 defaultVoltageOut = Voltage.VOLTAGE_230
664 break
fba11dc6 665 case CurrentType.DC:
66a7748d
JB
666 defaultVoltageOut = Voltage.VOLTAGE_400
667 break
fba11dc6 668 default:
66a7748d
JB
669 logger.error(`${logPrefix} ${errorMsg}`)
670 throw new BaseError(errorMsg)
15068be9 671 }
66a7748d
JB
672 return defaultVoltageOut
673}
fba11dc6
JB
674
675export const getIdTagsFile = (stationInfo: ChargingStationInfo): string | undefined => {
66a7748d
JB
676 return stationInfo.idTagsFile != null
677 ? join(dirname(fileURLToPath(import.meta.url)), 'assets', basename(stationInfo.idTagsFile))
678 : undefined
679}
fba11dc6 680
b2b60626 681export const waitChargingStationEvents = async (
fba11dc6
JB
682 emitter: EventEmitter,
683 event: ChargingStationWorkerMessageEvents,
66a7748d 684 eventsToWait: number
fba11dc6 685): Promise<number> => {
a974c8e4 686 return await new Promise<number>(resolve => {
66a7748d 687 let events = 0
fba11dc6 688 if (eventsToWait === 0) {
66a7748d
JB
689 resolve(events)
690 return
fba11dc6
JB
691 }
692 emitter.on(event, () => {
66a7748d 693 ++events
fba11dc6 694 if (events === eventsToWait) {
66a7748d 695 resolve(events)
b1f1b0f6 696 }
66a7748d
JB
697 })
698 })
699}
fba11dc6 700
cfc9875a 701const getConfiguredMaxNumberOfConnectors = (stationTemplate: ChargingStationTemplate): number => {
66a7748d
JB
702 let configuredMaxNumberOfConnectors = 0
703 if (isNotEmptyArray(stationTemplate.numberOfConnectors)) {
5dc7c990 704 const numberOfConnectors = stationTemplate.numberOfConnectors
66a7748d
JB
705 configuredMaxNumberOfConnectors =
706 numberOfConnectors[Math.floor(secureRandom() * numberOfConnectors.length)]
300418e9 707 } else if (stationTemplate.numberOfConnectors != null) {
5dc7c990 708 configuredMaxNumberOfConnectors = stationTemplate.numberOfConnectors
66a7748d 709 } else if (stationTemplate.Connectors != null && stationTemplate.Evses == null) {
cfc9875a 710 configuredMaxNumberOfConnectors =
5199f9fd
JB
711 // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
712 stationTemplate.Connectors[0] != null
66a7748d
JB
713 ? getMaxNumberOfConnectors(stationTemplate.Connectors) - 1
714 : getMaxNumberOfConnectors(stationTemplate.Connectors)
715 } else if (stationTemplate.Evses != null && stationTemplate.Connectors == null) {
fba11dc6
JB
716 for (const evse in stationTemplate.Evses) {
717 if (evse === '0') {
66a7748d 718 continue
cda5d0fb 719 }
cfc9875a 720 configuredMaxNumberOfConnectors += getMaxNumberOfConnectors(
66a7748d
JB
721 stationTemplate.Evses[evse].Connectors
722 )
cda5d0fb 723 }
cda5d0fb 724 }
66a7748d
JB
725 return configuredMaxNumberOfConnectors
726}
fba11dc6
JB
727
728const checkConfiguredMaxConnectors = (
729 configuredMaxConnectors: number,
730 logPrefix: string,
66a7748d 731 templateFile: string
fba11dc6
JB
732): void => {
733 if (configuredMaxConnectors <= 0) {
734 logger.warn(
66a7748d
JB
735 `${logPrefix} Charging station information from template ${templateFile} with ${configuredMaxConnectors} connectors`
736 )
cda5d0fb 737 }
66a7748d 738}
cda5d0fb 739
fba11dc6
JB
740const checkTemplateMaxConnectors = (
741 templateMaxConnectors: number,
742 logPrefix: string,
66a7748d 743 templateFile: string
fba11dc6
JB
744): void => {
745 if (templateMaxConnectors === 0) {
746 logger.warn(
66a7748d
JB
747 `${logPrefix} Charging station information from template ${templateFile} with empty connectors configuration`
748 )
fba11dc6
JB
749 } else if (templateMaxConnectors < 0) {
750 logger.error(
66a7748d
JB
751 `${logPrefix} Charging station information from template ${templateFile} with no connectors configuration defined`
752 )
fba11dc6 753 }
66a7748d 754}
fba11dc6
JB
755
756const initializeConnectorStatus = (connectorStatus: ConnectorStatus): void => {
66a7748d
JB
757 connectorStatus.availability = AvailabilityType.Operative
758 connectorStatus.idTagLocalAuthorized = false
759 connectorStatus.idTagAuthorized = false
760 connectorStatus.transactionRemoteStarted = false
761 connectorStatus.transactionStarted = false
762 connectorStatus.energyActiveImportRegisterValue = 0
763 connectorStatus.transactionEnergyActiveImportRegisterValue = 0
300418e9 764 if (connectorStatus.chargingProfiles == null) {
66a7748d 765 connectorStatus.chargingProfiles = []
fba11dc6 766 }
66a7748d 767}
fba11dc6
JB
768
769const warnDeprecatedTemplateKey = (
770 template: ChargingStationTemplate,
771 key: string,
772 logPrefix: string,
773 templateFile: string,
66a7748d 774 logMsgToAppend = ''
fba11dc6 775): void => {
d760a0a6 776 if (template[key as keyof ChargingStationTemplate] != null) {
fba11dc6
JB
777 const logMsg = `Deprecated template key '${key}' usage in file '${templateFile}'${
778 isNotEmptyString(logMsgToAppend) ? `. ${logMsgToAppend}` : ''
66a7748d
JB
779 }`
780 logger.warn(`${logPrefix} ${logMsg}`)
781 console.warn(`${chalk.green(logPrefix)} ${chalk.yellow(logMsg)}`)
fba11dc6 782 }
66a7748d 783}
fba11dc6
JB
784
785const convertDeprecatedTemplateKey = (
786 template: ChargingStationTemplate,
787 deprecatedKey: string,
66a7748d 788 key?: string
fba11dc6 789): void => {
d760a0a6
JB
790 if (template[deprecatedKey as keyof ChargingStationTemplate] != null) {
791 if (key != null) {
300418e9 792 (template as unknown as Record<string, unknown>)[key] =
66a7748d 793 template[deprecatedKey as keyof ChargingStationTemplate]
e1d9a0f4 794 }
66a7748d
JB
795 // eslint-disable-next-line @typescript-eslint/no-dynamic-delete
796 delete template[deprecatedKey as keyof ChargingStationTemplate]
fba11dc6 797 }
66a7748d 798}
fba11dc6 799
947f048a 800interface ChargingProfilesLimit {
66a7748d
JB
801 limit: number
802 chargingProfile: ChargingProfile
947f048a
JB
803}
804
fba11dc6 805/**
21ee4dc2 806 * Charging profiles shall already be sorted by connector id descending then stack level descending
fba11dc6 807 *
d467756c
JB
808 * @param chargingStation -
809 * @param connectorId -
fba11dc6
JB
810 * @param chargingProfiles -
811 * @param logPrefix -
947f048a 812 * @returns ChargingProfilesLimit
fba11dc6
JB
813 */
814const getLimitFromChargingProfiles = (
a71d4e70
JB
815 chargingStation: ChargingStation,
816 connectorId: number,
fba11dc6 817 chargingProfiles: ChargingProfile[],
66a7748d 818 logPrefix: string
947f048a 819): ChargingProfilesLimit | undefined => {
66a7748d
JB
820 const debugLogMsg = `${logPrefix} ${moduleName}.getLimitFromChargingProfiles: Matching charging profile found for power limitation: %j`
821 const currentDate = new Date()
f938317f 822 const connectorStatus = chargingStation.getConnectorStatus(connectorId)
fba11dc6 823 for (const chargingProfile of chargingProfiles) {
66a7748d 824 const chargingSchedule = chargingProfile.chargingSchedule
2466918c 825 if (chargingSchedule.startSchedule == null) {
109c677a 826 logger.debug(
66a7748d
JB
827 `${logPrefix} ${moduleName}.getLimitFromChargingProfiles: Charging profile id ${chargingProfile.chargingProfileId} has no startSchedule defined. Trying to set it to the connector current transaction start date`
828 )
a71d4e70 829 // OCPP specifies that if startSchedule is not defined, it should be relative to start of the connector transaction
f938317f 830 chargingSchedule.startSchedule = connectorStatus?.transactionStart
52952bf8 831 }
2466918c 832 if (!isDate(chargingSchedule.startSchedule)) {
ef9e3b33 833 logger.warn(
66a7748d
JB
834 `${logPrefix} ${moduleName}.getLimitFromChargingProfiles: Charging profile id ${chargingProfile.chargingProfileId} startSchedule property is not a Date instance. Trying to convert it to a Date instance`
835 )
836 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
5199f9fd 837 chargingSchedule.startSchedule = convertToDate(chargingSchedule.startSchedule)!
ef9e3b33 838 }
2466918c 839 if (chargingSchedule.duration == null) {
da332e70 840 logger.debug(
66a7748d
JB
841 `${logPrefix} ${moduleName}.getLimitFromChargingProfiles: Charging profile id ${chargingProfile.chargingProfileId} has no duration defined and will be set to the maximum time allowed`
842 )
da332e70 843 // OCPP specifies that if duration is not defined, it should be infinite
be9f397b 844 chargingSchedule.duration = differenceInSeconds(maxTime, chargingSchedule.startSchedule)
da332e70 845 }
0eb666db 846 if (!prepareChargingProfileKind(connectorStatus, chargingProfile, currentDate, logPrefix)) {
66a7748d 847 continue
ec4a242a 848 }
0bd926c1 849 if (!canProceedChargingProfile(chargingProfile, currentDate, logPrefix)) {
66a7748d 850 continue
142a66c9 851 }
fba11dc6
JB
852 // Check if the charging profile is active
853 if (
975e18ec 854 isWithinInterval(currentDate, {
2466918c
JB
855 start: chargingSchedule.startSchedule,
856 end: addSeconds(chargingSchedule.startSchedule, chargingSchedule.duration)
975e18ec 857 })
fba11dc6 858 ) {
252a7d22 859 if (isNotEmptyArray(chargingSchedule.chargingSchedulePeriod)) {
80c58041
JB
860 const chargingSchedulePeriodCompareFn = (
861 a: ChargingSchedulePeriod,
66a7748d
JB
862 b: ChargingSchedulePeriod
863 ): number => a.startPeriod - b.startPeriod
80c58041 864 if (
6fc0c6f3 865 !isArraySorted<ChargingSchedulePeriod>(
80c58041 866 chargingSchedule.chargingSchedulePeriod,
66a7748d 867 chargingSchedulePeriodCompareFn
6fc0c6f3 868 )
80c58041
JB
869 ) {
870 logger.warn(
66a7748d
JB
871 `${logPrefix} ${moduleName}.getLimitFromChargingProfiles: Charging profile id ${chargingProfile.chargingProfileId} schedule periods are not sorted by start period`
872 )
873 chargingSchedule.chargingSchedulePeriod.sort(chargingSchedulePeriodCompareFn)
80c58041 874 }
da332e70 875 // Check if the first schedule period startPeriod property is equal to 0
55f2ab60
JB
876 if (chargingSchedule.chargingSchedulePeriod[0].startPeriod !== 0) {
877 logger.error(
66a7748d
JB
878 `${logPrefix} ${moduleName}.getLimitFromChargingProfiles: Charging profile id ${chargingProfile.chargingProfileId} first schedule period start period ${chargingSchedule.chargingSchedulePeriod[0].startPeriod} is not equal to 0`
879 )
880 continue
55f2ab60 881 }
991fb26b 882 // Handle only one schedule period
975e18ec 883 if (chargingSchedule.chargingSchedulePeriod.length === 1) {
252a7d22
JB
884 const result: ChargingProfilesLimit = {
885 limit: chargingSchedule.chargingSchedulePeriod[0].limit,
66a7748d
JB
886 chargingProfile
887 }
888 logger.debug(debugLogMsg, result)
889 return result
41189456 890 }
66a7748d 891 let previousChargingSchedulePeriod: ChargingSchedulePeriod | undefined
252a7d22 892 // Search for the right schedule period
e3037969
JB
893 for (const [
894 index,
66a7748d 895 chargingSchedulePeriod
e3037969 896 ] of chargingSchedule.chargingSchedulePeriod.entries()) {
252a7d22
JB
897 // Find the right schedule period
898 if (
899 isAfter(
2466918c 900 addSeconds(chargingSchedule.startSchedule, chargingSchedulePeriod.startPeriod),
66a7748d 901 currentDate
252a7d22
JB
902 )
903 ) {
e3037969 904 // Found the schedule period: previous is the correct one
252a7d22 905 const result: ChargingProfilesLimit = {
66a7748d 906 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
e3037969 907 limit: previousChargingSchedulePeriod!.limit,
66a7748d
JB
908 chargingProfile
909 }
910 logger.debug(debugLogMsg, result)
911 return result
252a7d22 912 }
e3037969 913 // Keep a reference to previous one
66a7748d 914 previousChargingSchedulePeriod = chargingSchedulePeriod
975e18ec 915 // Handle the last schedule period within the charging profile duration
252a7d22 916 if (
975e18ec
JB
917 index === chargingSchedule.chargingSchedulePeriod.length - 1 ||
918 (index < chargingSchedule.chargingSchedulePeriod.length - 1 &&
ccfa30bc
JB
919 differenceInSeconds(
920 addSeconds(
2466918c 921 chargingSchedule.startSchedule,
66a7748d 922 chargingSchedule.chargingSchedulePeriod[index + 1].startPeriod
ccfa30bc 923 ),
2466918c
JB
924 chargingSchedule.startSchedule
925 ) > chargingSchedule.duration)
252a7d22
JB
926 ) {
927 const result: ChargingProfilesLimit = {
e3037969 928 limit: previousChargingSchedulePeriod.limit,
66a7748d
JB
929 chargingProfile
930 }
931 logger.debug(debugLogMsg, result)
932 return result
252a7d22 933 }
17ac262c
JB
934 }
935 }
936 }
17ac262c 937 }
66a7748d 938}
17ac262c 939
0eb666db 940export const prepareChargingProfileKind = (
f938317f 941 connectorStatus: ConnectorStatus | undefined,
0eb666db 942 chargingProfile: ChargingProfile,
6dde6c5f 943 currentDate: string | number | Date,
66a7748d 944 logPrefix: string
0eb666db
JB
945): boolean => {
946 switch (chargingProfile.chargingProfileKind) {
947 case ChargingProfileKindType.RECURRING:
948 if (!canProceedRecurringChargingProfile(chargingProfile, logPrefix)) {
66a7748d 949 return false
0eb666db 950 }
66a7748d
JB
951 prepareRecurringChargingProfile(chargingProfile, currentDate, logPrefix)
952 break
0eb666db 953 case ChargingProfileKindType.RELATIVE:
be9f397b 954 if (chargingProfile.chargingSchedule.startSchedule != null) {
ccfa30bc 955 logger.warn(
66a7748d
JB
956 `${logPrefix} ${moduleName}.prepareChargingProfileKind: Relative charging profile id ${chargingProfile.chargingProfileId} has a startSchedule property defined. It will be ignored or used if the connector has a transaction started`
957 )
958 delete chargingProfile.chargingSchedule.startSchedule
ccfa30bc 959 }
f938317f 960 if (connectorStatus?.transactionStarted === true) {
5199f9fd 961 chargingProfile.chargingSchedule.startSchedule = connectorStatus.transactionStart
ef9e3b33
JB
962 }
963 // FIXME: Handle relative charging profile duration
66a7748d 964 break
0eb666db 965 }
66a7748d
JB
966 return true
967}
0eb666db 968
ad490d5f 969export const canProceedChargingProfile = (
0bd926c1 970 chargingProfile: ChargingProfile,
6dde6c5f 971 currentDate: string | number | Date,
66a7748d 972 logPrefix: string
0bd926c1
JB
973): boolean => {
974 if (
5dc7c990
JB
975 (isValidDate(chargingProfile.validFrom) && isBefore(currentDate, chargingProfile.validFrom)) ||
976 (isValidDate(chargingProfile.validTo) && isAfter(currentDate, chargingProfile.validTo))
0bd926c1
JB
977 ) {
978 logger.debug(
979 `${logPrefix} ${moduleName}.canProceedChargingProfile: Charging profile id ${
980 chargingProfile.chargingProfileId
6dde6c5f 981 } is not valid for the current date ${
5dc7c990 982 isDate(currentDate) ? currentDate.toISOString() : currentDate
6dde6c5f 983 }`
66a7748d
JB
984 )
985 return false
0bd926c1 986 }
ef9e3b33 987 if (
be9f397b
JB
988 chargingProfile.chargingSchedule.startSchedule == null ||
989 chargingProfile.chargingSchedule.duration == null
ef9e3b33 990 ) {
0bd926c1 991 logger.error(
66a7748d
JB
992 `${logPrefix} ${moduleName}.canProceedChargingProfile: Charging profile id ${chargingProfile.chargingProfileId} has no startSchedule or duration defined`
993 )
994 return false
ef9e3b33 995 }
5dc7c990 996 if (!isValidDate(chargingProfile.chargingSchedule.startSchedule)) {
ef9e3b33 997 logger.error(
66a7748d
JB
998 `${logPrefix} ${moduleName}.canProceedChargingProfile: Charging profile id ${chargingProfile.chargingProfileId} has an invalid startSchedule date defined`
999 )
1000 return false
ef9e3b33 1001 }
5199f9fd 1002 if (!Number.isSafeInteger(chargingProfile.chargingSchedule.duration)) {
ef9e3b33 1003 logger.error(
66a7748d
JB
1004 `${logPrefix} ${moduleName}.canProceedChargingProfile: Charging profile id ${chargingProfile.chargingProfileId} has non integer duration defined`
1005 )
1006 return false
0bd926c1 1007 }
66a7748d
JB
1008 return true
1009}
0bd926c1 1010
0eb666db 1011const canProceedRecurringChargingProfile = (
0bd926c1 1012 chargingProfile: ChargingProfile,
66a7748d 1013 logPrefix: string
0bd926c1
JB
1014): boolean => {
1015 if (
1016 chargingProfile.chargingProfileKind === ChargingProfileKindType.RECURRING &&
be9f397b 1017 chargingProfile.recurrencyKind == null
0bd926c1
JB
1018 ) {
1019 logger.error(
66a7748d
JB
1020 `${logPrefix} ${moduleName}.canProceedRecurringChargingProfile: Recurring charging profile id ${chargingProfile.chargingProfileId} has no recurrencyKind defined`
1021 )
1022 return false
0bd926c1 1023 }
d929adcc
JB
1024 if (
1025 chargingProfile.chargingProfileKind === ChargingProfileKindType.RECURRING &&
be9f397b 1026 chargingProfile.chargingSchedule.startSchedule == null
d929adcc 1027 ) {
ef9e3b33 1028 logger.error(
66a7748d
JB
1029 `${logPrefix} ${moduleName}.canProceedRecurringChargingProfile: Recurring charging profile id ${chargingProfile.chargingProfileId} has no startSchedule defined`
1030 )
1031 return false
ef9e3b33 1032 }
66a7748d
JB
1033 return true
1034}
0bd926c1 1035
522e4b05 1036/**
ec4a242a 1037 * Adjust recurring charging profile startSchedule to the current recurrency time interval if needed
522e4b05
JB
1038 *
1039 * @param chargingProfile -
1040 * @param currentDate -
1041 * @param logPrefix -
1042 */
0eb666db 1043const prepareRecurringChargingProfile = (
76dab5a9 1044 chargingProfile: ChargingProfile,
6dde6c5f 1045 currentDate: string | number | Date,
66a7748d 1046 logPrefix: string
ec4a242a 1047): boolean => {
66a7748d
JB
1048 const chargingSchedule = chargingProfile.chargingSchedule
1049 let recurringIntervalTranslated = false
5dc7c990 1050 let recurringInterval: Interval | undefined
76dab5a9
JB
1051 switch (chargingProfile.recurrencyKind) {
1052 case RecurrencyKindType.DAILY:
522e4b05 1053 recurringInterval = {
66a7748d 1054 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
522e4b05 1055 start: chargingSchedule.startSchedule!,
66a7748d
JB
1056 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
1057 end: addDays(chargingSchedule.startSchedule!, 1)
1058 }
1059 checkRecurringChargingProfileDuration(chargingProfile, recurringInterval, logPrefix)
522e4b05
JB
1060 if (
1061 !isWithinInterval(currentDate, recurringInterval) &&
991fb26b 1062 isBefore(recurringInterval.end, currentDate)
522e4b05
JB
1063 ) {
1064 chargingSchedule.startSchedule = addDays(
991fb26b 1065 recurringInterval.start,
66a7748d
JB
1066 differenceInDays(currentDate, recurringInterval.start)
1067 )
522e4b05
JB
1068 recurringInterval = {
1069 start: chargingSchedule.startSchedule,
66a7748d
JB
1070 end: addDays(chargingSchedule.startSchedule, 1)
1071 }
1072 recurringIntervalTranslated = true
76dab5a9 1073 }
66a7748d 1074 break
76dab5a9 1075 case RecurrencyKindType.WEEKLY:
522e4b05 1076 recurringInterval = {
66a7748d 1077 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
522e4b05 1078 start: chargingSchedule.startSchedule!,
66a7748d
JB
1079 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
1080 end: addWeeks(chargingSchedule.startSchedule!, 1)
1081 }
1082 checkRecurringChargingProfileDuration(chargingProfile, recurringInterval, logPrefix)
522e4b05
JB
1083 if (
1084 !isWithinInterval(currentDate, recurringInterval) &&
991fb26b 1085 isBefore(recurringInterval.end, currentDate)
522e4b05
JB
1086 ) {
1087 chargingSchedule.startSchedule = addWeeks(
991fb26b 1088 recurringInterval.start,
66a7748d
JB
1089 differenceInWeeks(currentDate, recurringInterval.start)
1090 )
522e4b05
JB
1091 recurringInterval = {
1092 start: chargingSchedule.startSchedule,
66a7748d
JB
1093 end: addWeeks(chargingSchedule.startSchedule, 1)
1094 }
1095 recurringIntervalTranslated = true
76dab5a9 1096 }
66a7748d 1097 break
ec4a242a
JB
1098 default:
1099 logger.error(
66a7748d
JB
1100 `${logPrefix} ${moduleName}.prepareRecurringChargingProfile: Recurring ${chargingProfile.recurrencyKind} charging profile id ${chargingProfile.chargingProfileId} is not supported`
1101 )
76dab5a9 1102 }
66a7748d 1103 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
ec4a242a 1104 if (recurringIntervalTranslated && !isWithinInterval(currentDate, recurringInterval!)) {
522e4b05 1105 logger.error(
aa5c5ad4 1106 `${logPrefix} ${moduleName}.prepareRecurringChargingProfile: Recurring ${
522e4b05 1107 chargingProfile.recurrencyKind
991fb26b 1108 } charging profile id ${chargingProfile.chargingProfileId} recurrency time interval [${toDate(
5dc7c990 1109 recurringInterval?.start as Date
991fb26b 1110 ).toISOString()}, ${toDate(
5dc7c990 1111 recurringInterval?.end as Date
6dde6c5f 1112 ).toISOString()}] has not been properly translated to current date ${
5dc7c990 1113 isDate(currentDate) ? currentDate.toISOString() : currentDate
6dde6c5f 1114 } `
66a7748d 1115 )
522e4b05 1116 }
66a7748d
JB
1117 return recurringIntervalTranslated
1118}
76dab5a9 1119
d476bc1b
JB
1120const checkRecurringChargingProfileDuration = (
1121 chargingProfile: ChargingProfile,
1122 interval: Interval,
66a7748d 1123 logPrefix: string
ec4a242a 1124): void => {
be9f397b 1125 if (chargingProfile.chargingSchedule.duration == null) {
142a66c9
JB
1126 logger.warn(
1127 `${logPrefix} ${moduleName}.checkRecurringChargingProfileDuration: Recurring ${
1128 chargingProfile.chargingProfileKind
1129 } charging profile id ${
1130 chargingProfile.chargingProfileId
1131 } duration is not defined, set it to the recurrency time interval duration ${differenceInSeconds(
1132 interval.end,
66a7748d
JB
1133 interval.start
1134 )}`
1135 )
1136 chargingProfile.chargingSchedule.duration = differenceInSeconds(interval.end, interval.start)
142a66c9 1137 } else if (
be9f397b 1138 chargingProfile.chargingSchedule.duration > differenceInSeconds(interval.end, interval.start)
d476bc1b
JB
1139 ) {
1140 logger.warn(
aa5c5ad4 1141 `${logPrefix} ${moduleName}.checkRecurringChargingProfileDuration: Recurring ${
d476bc1b
JB
1142 chargingProfile.chargingProfileKind
1143 } charging profile id ${chargingProfile.chargingProfileId} duration ${
1144 chargingProfile.chargingSchedule.duration
710d50eb 1145 } is greater than the recurrency time interval duration ${differenceInSeconds(
d476bc1b 1146 interval.end,
66a7748d
JB
1147 interval.start
1148 )}`
1149 )
1150 chargingProfile.chargingSchedule.duration = differenceInSeconds(interval.end, interval.start)
d476bc1b 1151 }
66a7748d 1152}
d476bc1b 1153
fba11dc6 1154const getRandomSerialNumberSuffix = (params?: {
66a7748d
JB
1155 randomBytesLength?: number
1156 upperCase?: boolean
fba11dc6 1157}): string => {
66a7748d
JB
1158 const randomSerialNumberSuffix = randomBytes(params?.randomBytesLength ?? 16).toString('hex')
1159 if (params?.upperCase === true) {
1160 return randomSerialNumberSuffix.toUpperCase()
17ac262c 1161 }
66a7748d
JB
1162 return randomSerialNumberSuffix
1163}