refactor: cleanup charging station options handling
[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,
300418e9 149 ...(stationTemplate.chargeBoxSerialNumberPrefix !== undefined && {
66a7748d 150 chargeBoxSerialNumber: stationTemplate.chargeBoxSerialNumberPrefix
fba11dc6 151 }),
300418e9 152 ...(stationTemplate.chargePointSerialNumberPrefix !== undefined && {
66a7748d 153 chargePointSerialNumber: stationTemplate.chargePointSerialNumberPrefix
fba11dc6 154 }),
300418e9 155 ...(stationTemplate.meterSerialNumberPrefix !== undefined && {
66a7748d 156 meterSerialNumber: stationTemplate.meterSerialNumberPrefix
fba11dc6 157 }),
300418e9 158 ...(stationTemplate.meterType !== undefined && {
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) {
342 stationInfo.ocppPersistentConfiguration = options.persistentConfiguration
343 stationInfo.automaticTransactionGeneratorPersistentConfiguration =
344 options.persistentConfiguration
345 }
346 if (options?.autoRegister != null) {
347 stationInfo.autoRegister = options.autoRegister
348 }
349 if (options?.enableStatistics != null) {
350 stationInfo.enableStatistics = options.enableStatistics
351 }
352 if (options?.ocppStrictCompliance != null) {
353 stationInfo.ocppStrictCompliance = options.ocppStrictCompliance
354 }
355 if (options?.stopTransactionsOnStopped != null) {
356 stationInfo.stopTransactionsOnStopped = options.stopTransactionsOnStopped
357 }
358 return stationInfo
359}
360
fba11dc6
JB
361export const initializeConnectorsMapStatus = (
362 connectors: Map<number, ConnectorStatus>,
66a7748d 363 logPrefix: string
fba11dc6
JB
364): void => {
365 for (const connectorId of connectors.keys()) {
366 if (connectorId > 0 && connectors.get(connectorId)?.transactionStarted === true) {
04b1261c 367 logger.warn(
a223d9be
JB
368 `${logPrefix} Connector id ${connectorId} at initialization has a transaction started with id ${
369 connectors.get(connectorId)?.transactionId
370 }`
66a7748d 371 )
04b1261c 372 }
fba11dc6 373 if (connectorId === 0) {
66a7748d
JB
374 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
375 connectors.get(connectorId)!.availability = AvailabilityType.Operative
300418e9 376 if (connectors.get(connectorId)?.chargingProfiles == null) {
66a7748d
JB
377 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
378 connectors.get(connectorId)!.chargingProfiles = []
04b1261c 379 }
be9f397b 380 } else if (connectorId > 0 && connectors.get(connectorId)?.transactionStarted == null) {
66a7748d
JB
381 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
382 initializeConnectorStatus(connectors.get(connectorId)!)
04b1261c
JB
383 }
384 }
66a7748d 385}
fba11dc6 386
f938317f
JB
387export const resetConnectorStatus = (connectorStatus: ConnectorStatus | undefined): void => {
388 if (connectorStatus == null) {
389 return
390 }
86f51b96 391 connectorStatus.chargingProfiles =
66a7748d 392 connectorStatus.transactionId != null && isNotEmptyArray(connectorStatus.chargingProfiles)
5dc7c990 393 ? connectorStatus.chargingProfiles.filter(
a974c8e4 394 chargingProfile => chargingProfile.transactionId !== connectorStatus.transactionId
66a7748d
JB
395 )
396 : []
397 connectorStatus.idTagLocalAuthorized = false
398 connectorStatus.idTagAuthorized = false
399 connectorStatus.transactionRemoteStarted = false
400 connectorStatus.transactionStarted = false
5199f9fd
JB
401 delete connectorStatus.transactionStart
402 delete connectorStatus.transactionId
403 delete connectorStatus.localAuthorizeIdTag
404 delete connectorStatus.authorizeIdTag
405 delete connectorStatus.transactionIdTag
66a7748d 406 connectorStatus.transactionEnergyActiveImportRegisterValue = 0
5199f9fd 407 delete connectorStatus.transactionBeginMeterValue
66a7748d 408}
fba11dc6
JB
409
410export const createBootNotificationRequest = (
411 stationInfo: ChargingStationInfo,
66a7748d 412 bootReason: BootReasonEnumType = BootReasonEnumType.PowerUp
2466918c
JB
413): BootNotificationRequest | undefined => {
414 const ocppVersion = stationInfo.ocppVersion
fba11dc6
JB
415 switch (ocppVersion) {
416 case OCPPVersion.VERSION_16:
417 return {
418 chargePointModel: stationInfo.chargePointModel,
419 chargePointVendor: stationInfo.chargePointVendor,
300418e9 420 ...(stationInfo.chargeBoxSerialNumber !== undefined && {
66a7748d 421 chargeBoxSerialNumber: stationInfo.chargeBoxSerialNumber
fba11dc6 422 }),
300418e9 423 ...(stationInfo.chargePointSerialNumber !== undefined && {
66a7748d 424 chargePointSerialNumber: stationInfo.chargePointSerialNumber
fba11dc6 425 }),
300418e9 426 ...(stationInfo.firmwareVersion !== undefined && {
66a7748d 427 firmwareVersion: stationInfo.firmwareVersion
fba11dc6 428 }),
300418e9
JB
429 ...(stationInfo.iccid !== undefined && { iccid: stationInfo.iccid }),
430 ...(stationInfo.imsi !== undefined && { imsi: stationInfo.imsi }),
431 ...(stationInfo.meterSerialNumber !== undefined && {
66a7748d 432 meterSerialNumber: stationInfo.meterSerialNumber
fba11dc6 433 }),
300418e9 434 ...(stationInfo.meterType !== undefined && {
66a7748d
JB
435 meterType: stationInfo.meterType
436 })
437 } satisfies OCPP16BootNotificationRequest
fba11dc6
JB
438 case OCPPVersion.VERSION_20:
439 case OCPPVersion.VERSION_201:
440 return {
441 reason: bootReason,
442 chargingStation: {
443 model: stationInfo.chargePointModel,
444 vendorName: stationInfo.chargePointVendor,
300418e9 445 ...(stationInfo.firmwareVersion !== undefined && {
66a7748d 446 firmwareVersion: stationInfo.firmwareVersion
d270cc87 447 }),
300418e9 448 ...(stationInfo.chargeBoxSerialNumber !== undefined && {
66a7748d 449 serialNumber: stationInfo.chargeBoxSerialNumber
d270cc87 450 }),
300418e9 451 ...((stationInfo.iccid !== undefined || stationInfo.imsi !== undefined) && {
fba11dc6 452 modem: {
300418e9
JB
453 ...(stationInfo.iccid !== undefined && { iccid: stationInfo.iccid }),
454 ...(stationInfo.imsi !== undefined && { imsi: stationInfo.imsi })
66a7748d
JB
455 }
456 })
457 }
458 } satisfies OCPP20BootNotificationRequest
fba11dc6 459 }
66a7748d 460}
fba11dc6
JB
461
462export const warnTemplateKeysDeprecation = (
463 stationTemplate: ChargingStationTemplate,
464 logPrefix: string,
66a7748d
JB
465 templateFile: string
466): void => {
467 const templateKeys: Array<{ deprecatedKey: string, key?: string }> = [
e4c6cf05
JB
468 { deprecatedKey: 'supervisionUrl', key: 'supervisionUrls' },
469 { deprecatedKey: 'authorizationFile', key: 'idTagsFile' },
470 { deprecatedKey: 'payloadSchemaValidation', key: 'ocppStrictCompliance' },
66a7748d
JB
471 { deprecatedKey: 'mustAuthorizeAtRemoteStart', key: 'remoteAuthorization' }
472 ]
fba11dc6
JB
473 for (const templateKey of templateKeys) {
474 warnDeprecatedTemplateKey(
475 stationTemplate,
476 templateKey.deprecatedKey,
477 logPrefix,
478 templateFile,
300418e9 479 templateKey.key !== undefined ? `Use '${templateKey.key}' instead` : undefined
66a7748d
JB
480 )
481 convertDeprecatedTemplateKey(stationTemplate, templateKey.deprecatedKey, templateKey.key)
fba11dc6 482 }
66a7748d 483}
fba11dc6
JB
484
485export const stationTemplateToStationInfo = (
66a7748d 486 stationTemplate: ChargingStationTemplate
fba11dc6 487): ChargingStationInfo => {
40615072 488 stationTemplate = clone<ChargingStationTemplate>(stationTemplate)
66a7748d
JB
489 delete stationTemplate.power
490 delete stationTemplate.powerUnit
491 delete stationTemplate.Connectors
492 delete stationTemplate.Evses
493 delete stationTemplate.Configuration
494 delete stationTemplate.AutomaticTransactionGenerator
495 delete stationTemplate.chargeBoxSerialNumberPrefix
496 delete stationTemplate.chargePointSerialNumberPrefix
497 delete stationTemplate.meterSerialNumberPrefix
498 return stationTemplate as ChargingStationInfo
499}
fba11dc6
JB
500
501export const createSerialNumber = (
502 stationTemplate: ChargingStationTemplate,
503 stationInfo: ChargingStationInfo,
7f3decca 504 params?: {
66a7748d
JB
505 randomSerialNumberUpperCase?: boolean
506 randomSerialNumber?: boolean
507 }
fba11dc6 508): void => {
66a7748d
JB
509 params = { ...{ randomSerialNumberUpperCase: true, randomSerialNumber: true }, ...params }
510 const serialNumberSuffix =
5199f9fd 511 params.randomSerialNumber === true
66a7748d
JB
512 ? getRandomSerialNumberSuffix({
513 upperCase: params.randomSerialNumberUpperCase
fba11dc6 514 })
66a7748d 515 : ''
5199f9fd 516 isNotEmptyString(stationTemplate.chargePointSerialNumberPrefix) &&
66a7748d 517 (stationInfo.chargePointSerialNumber = `${stationTemplate.chargePointSerialNumberPrefix}${serialNumberSuffix}`)
5199f9fd 518 isNotEmptyString(stationTemplate.chargeBoxSerialNumberPrefix) &&
66a7748d 519 (stationInfo.chargeBoxSerialNumber = `${stationTemplate.chargeBoxSerialNumberPrefix}${serialNumberSuffix}`)
5199f9fd 520 isNotEmptyString(stationTemplate.meterSerialNumberPrefix) &&
66a7748d
JB
521 (stationInfo.meterSerialNumber = `${stationTemplate.meterSerialNumberPrefix}${serialNumberSuffix}`)
522}
fba11dc6
JB
523
524export const propagateSerialNumber = (
5199f9fd
JB
525 stationTemplate: ChargingStationTemplate | undefined,
526 stationInfoSrc: ChargingStationInfo | undefined,
66a7748d
JB
527 stationInfoDst: ChargingStationInfo
528): void => {
529 if (stationInfoSrc == null || stationTemplate == null) {
fba11dc6 530 throw new BaseError(
66a7748d
JB
531 'Missing charging station template or existing configuration to propagate serial number'
532 )
17ac262c 533 }
5199f9fd
JB
534 stationTemplate.chargePointSerialNumberPrefix != null &&
535 stationInfoSrc.chargePointSerialNumber != null
fba11dc6 536 ? (stationInfoDst.chargePointSerialNumber = stationInfoSrc.chargePointSerialNumber)
5199f9fd 537 : stationInfoDst.chargePointSerialNumber != null &&
66a7748d 538 delete stationInfoDst.chargePointSerialNumber
5199f9fd
JB
539 stationTemplate.chargeBoxSerialNumberPrefix != null &&
540 stationInfoSrc.chargeBoxSerialNumber != null
fba11dc6 541 ? (stationInfoDst.chargeBoxSerialNumber = stationInfoSrc.chargeBoxSerialNumber)
5199f9fd
JB
542 : stationInfoDst.chargeBoxSerialNumber != null && delete stationInfoDst.chargeBoxSerialNumber
543 stationTemplate.meterSerialNumberPrefix != null && stationInfoSrc.meterSerialNumber != null
fba11dc6 544 ? (stationInfoDst.meterSerialNumber = stationInfoSrc.meterSerialNumber)
5199f9fd 545 : stationInfoDst.meterSerialNumber != null && delete stationInfoDst.meterSerialNumber
66a7748d 546}
fba11dc6 547
d8093be1
JB
548export const hasFeatureProfile = (
549 chargingStation: ChargingStation,
66a7748d 550 featureProfile: SupportedFeatureProfiles
d8093be1
JB
551): boolean | undefined => {
552 return getConfigurationKey(
553 chargingStation,
66a7748d
JB
554 StandardParametersKey.SupportedFeatureProfiles
555 )?.value?.includes(featureProfile)
556}
d8093be1 557
fba11dc6 558export const getAmperageLimitationUnitDivider = (stationInfo: ChargingStationInfo): number => {
66a7748d 559 let unitDivider = 1
fba11dc6
JB
560 switch (stationInfo.amperageLimitationUnit) {
561 case AmpereUnits.DECI_AMPERE:
66a7748d
JB
562 unitDivider = 10
563 break
fba11dc6 564 case AmpereUnits.CENTI_AMPERE:
66a7748d
JB
565 unitDivider = 100
566 break
fba11dc6 567 case AmpereUnits.MILLI_AMPERE:
66a7748d
JB
568 unitDivider = 1000
569 break
fba11dc6 570 }
66a7748d
JB
571 return unitDivider
572}
fba11dc6 573
6fc0c6f3
JB
574/**
575 * Gets the connector cloned charging profiles applying a power limitation
21ee4dc2 576 * and sorted by connector id descending then stack level descending
6fc0c6f3
JB
577 *
578 * @param chargingStation -
579 * @param connectorId -
580 * @returns connector charging profiles array
581 */
582export const getConnectorChargingProfiles = (
583 chargingStation: ChargingStation,
66a7748d
JB
584 connectorId: number
585): ChargingProfile[] => {
40615072 586 return clone<ChargingProfile[]>(
21ee4dc2 587 (chargingStation.getConnectorStatus(connectorId)?.chargingProfiles ?? [])
6fc0c6f3
JB
588 .sort((a, b) => b.stackLevel - a.stackLevel)
589 .concat(
21ee4dc2 590 (chargingStation.getConnectorStatus(0)?.chargingProfiles ?? []).sort(
66a7748d
JB
591 (a, b) => b.stackLevel - a.stackLevel
592 )
593 )
594 )
595}
6fc0c6f3 596
fba11dc6
JB
597export const getChargingStationConnectorChargingProfilesPowerLimit = (
598 chargingStation: ChargingStation,
66a7748d 599 connectorId: number
fba11dc6 600): number | undefined => {
66a7748d 601 let limit: number | undefined, chargingProfile: ChargingProfile | undefined
6fc0c6f3 602 // Get charging profiles sorted by connector id then stack level
66a7748d 603 const chargingProfiles = getConnectorChargingProfiles(chargingStation, connectorId)
fba11dc6 604 if (isNotEmptyArray(chargingProfiles)) {
a71d4e70
JB
605 const result = getLimitFromChargingProfiles(
606 chargingStation,
607 connectorId,
608 chargingProfiles,
66a7748d
JB
609 chargingStation.logPrefix()
610 )
be9f397b 611 if (result != null) {
5199f9fd
JB
612 limit = result.limit
613 chargingProfile = result.chargingProfile
5398cecf 614 switch (chargingStation.stationInfo?.currentOutType) {
fba11dc6
JB
615 case CurrentType.AC:
616 limit =
5199f9fd 617 chargingProfile.chargingSchedule.chargingRateUnit === ChargingRateUnitType.WATT
fba11dc6
JB
618 ? limit
619 : ACElectricUtils.powerTotal(
66a7748d
JB
620 chargingStation.getNumberOfPhases(),
621 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
622 chargingStation.stationInfo.voltageOut!,
be9f397b 623 limit
66a7748d
JB
624 )
625 break
fba11dc6
JB
626 case CurrentType.DC:
627 limit =
5199f9fd 628 chargingProfile.chargingSchedule.chargingRateUnit === ChargingRateUnitType.WATT
fba11dc6 629 ? limit
66a7748d 630 : // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
be9f397b 631 DCElectricUtils.power(chargingStation.stationInfo.voltageOut!, limit)
fba11dc6
JB
632 }
633 const connectorMaximumPower =
66a7748d 634 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
5199f9fd 635 chargingStation.stationInfo!.maximumPower! / chargingStation.powerDivider!
be9f397b 636 if (limit > connectorMaximumPower) {
fba11dc6 637 logger.error(
5199f9fd
JB
638 `${chargingStation.logPrefix()} ${moduleName}.getChargingStationConnectorChargingProfilesPowerLimit: Charging profile id ${
639 chargingProfile.chargingProfileId
640 } limit ${limit} is greater than connector id ${connectorId} maximum ${connectorMaximumPower}: %j`,
66a7748d
JB
641 result
642 )
643 limit = connectorMaximumPower
15068be9
JB
644 }
645 }
15068be9 646 }
66a7748d
JB
647 return limit
648}
fba11dc6
JB
649
650export const getDefaultVoltageOut = (
651 currentType: CurrentType,
652 logPrefix: string,
66a7748d 653 templateFile: string
fba11dc6 654): Voltage => {
66a7748d
JB
655 const errorMsg = `Unknown ${currentType} currentOutType in template file ${templateFile}, cannot define default voltage out`
656 let defaultVoltageOut: number
fba11dc6
JB
657 switch (currentType) {
658 case CurrentType.AC:
66a7748d
JB
659 defaultVoltageOut = Voltage.VOLTAGE_230
660 break
fba11dc6 661 case CurrentType.DC:
66a7748d
JB
662 defaultVoltageOut = Voltage.VOLTAGE_400
663 break
fba11dc6 664 default:
66a7748d
JB
665 logger.error(`${logPrefix} ${errorMsg}`)
666 throw new BaseError(errorMsg)
15068be9 667 }
66a7748d
JB
668 return defaultVoltageOut
669}
fba11dc6
JB
670
671export const getIdTagsFile = (stationInfo: ChargingStationInfo): string | undefined => {
66a7748d
JB
672 return stationInfo.idTagsFile != null
673 ? join(dirname(fileURLToPath(import.meta.url)), 'assets', basename(stationInfo.idTagsFile))
674 : undefined
675}
fba11dc6 676
b2b60626 677export const waitChargingStationEvents = async (
fba11dc6
JB
678 emitter: EventEmitter,
679 event: ChargingStationWorkerMessageEvents,
66a7748d 680 eventsToWait: number
fba11dc6 681): Promise<number> => {
a974c8e4 682 return await new Promise<number>(resolve => {
66a7748d 683 let events = 0
fba11dc6 684 if (eventsToWait === 0) {
66a7748d
JB
685 resolve(events)
686 return
fba11dc6
JB
687 }
688 emitter.on(event, () => {
66a7748d 689 ++events
fba11dc6 690 if (events === eventsToWait) {
66a7748d 691 resolve(events)
b1f1b0f6 692 }
66a7748d
JB
693 })
694 })
695}
fba11dc6 696
cfc9875a 697const getConfiguredMaxNumberOfConnectors = (stationTemplate: ChargingStationTemplate): number => {
66a7748d
JB
698 let configuredMaxNumberOfConnectors = 0
699 if (isNotEmptyArray(stationTemplate.numberOfConnectors)) {
5dc7c990 700 const numberOfConnectors = stationTemplate.numberOfConnectors
66a7748d
JB
701 configuredMaxNumberOfConnectors =
702 numberOfConnectors[Math.floor(secureRandom() * numberOfConnectors.length)]
300418e9 703 } else if (stationTemplate.numberOfConnectors != null) {
5dc7c990 704 configuredMaxNumberOfConnectors = stationTemplate.numberOfConnectors
66a7748d 705 } else if (stationTemplate.Connectors != null && stationTemplate.Evses == null) {
cfc9875a 706 configuredMaxNumberOfConnectors =
5199f9fd
JB
707 // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
708 stationTemplate.Connectors[0] != null
66a7748d
JB
709 ? getMaxNumberOfConnectors(stationTemplate.Connectors) - 1
710 : getMaxNumberOfConnectors(stationTemplate.Connectors)
711 } else if (stationTemplate.Evses != null && stationTemplate.Connectors == null) {
fba11dc6
JB
712 for (const evse in stationTemplate.Evses) {
713 if (evse === '0') {
66a7748d 714 continue
cda5d0fb 715 }
cfc9875a 716 configuredMaxNumberOfConnectors += getMaxNumberOfConnectors(
66a7748d
JB
717 stationTemplate.Evses[evse].Connectors
718 )
cda5d0fb 719 }
cda5d0fb 720 }
66a7748d
JB
721 return configuredMaxNumberOfConnectors
722}
fba11dc6
JB
723
724const checkConfiguredMaxConnectors = (
725 configuredMaxConnectors: number,
726 logPrefix: string,
66a7748d 727 templateFile: string
fba11dc6
JB
728): void => {
729 if (configuredMaxConnectors <= 0) {
730 logger.warn(
66a7748d
JB
731 `${logPrefix} Charging station information from template ${templateFile} with ${configuredMaxConnectors} connectors`
732 )
cda5d0fb 733 }
66a7748d 734}
cda5d0fb 735
fba11dc6
JB
736const checkTemplateMaxConnectors = (
737 templateMaxConnectors: number,
738 logPrefix: string,
66a7748d 739 templateFile: string
fba11dc6
JB
740): void => {
741 if (templateMaxConnectors === 0) {
742 logger.warn(
66a7748d
JB
743 `${logPrefix} Charging station information from template ${templateFile} with empty connectors configuration`
744 )
fba11dc6
JB
745 } else if (templateMaxConnectors < 0) {
746 logger.error(
66a7748d
JB
747 `${logPrefix} Charging station information from template ${templateFile} with no connectors configuration defined`
748 )
fba11dc6 749 }
66a7748d 750}
fba11dc6
JB
751
752const initializeConnectorStatus = (connectorStatus: ConnectorStatus): void => {
66a7748d
JB
753 connectorStatus.availability = AvailabilityType.Operative
754 connectorStatus.idTagLocalAuthorized = false
755 connectorStatus.idTagAuthorized = false
756 connectorStatus.transactionRemoteStarted = false
757 connectorStatus.transactionStarted = false
758 connectorStatus.energyActiveImportRegisterValue = 0
759 connectorStatus.transactionEnergyActiveImportRegisterValue = 0
300418e9 760 if (connectorStatus.chargingProfiles == null) {
66a7748d 761 connectorStatus.chargingProfiles = []
fba11dc6 762 }
66a7748d 763}
fba11dc6
JB
764
765const warnDeprecatedTemplateKey = (
766 template: ChargingStationTemplate,
767 key: string,
768 logPrefix: string,
769 templateFile: string,
66a7748d 770 logMsgToAppend = ''
fba11dc6 771): void => {
5199f9fd 772 if (template[key as keyof ChargingStationTemplate] !== undefined) {
fba11dc6
JB
773 const logMsg = `Deprecated template key '${key}' usage in file '${templateFile}'${
774 isNotEmptyString(logMsgToAppend) ? `. ${logMsgToAppend}` : ''
66a7748d
JB
775 }`
776 logger.warn(`${logPrefix} ${logMsg}`)
777 console.warn(`${chalk.green(logPrefix)} ${chalk.yellow(logMsg)}`)
fba11dc6 778 }
66a7748d 779}
fba11dc6
JB
780
781const convertDeprecatedTemplateKey = (
782 template: ChargingStationTemplate,
783 deprecatedKey: string,
66a7748d 784 key?: string
fba11dc6 785): void => {
5199f9fd 786 if (template[deprecatedKey as keyof ChargingStationTemplate] !== undefined) {
300418e9
JB
787 if (key !== undefined) {
788 (template as unknown as Record<string, unknown>)[key] =
66a7748d 789 template[deprecatedKey as keyof ChargingStationTemplate]
e1d9a0f4 790 }
66a7748d
JB
791 // eslint-disable-next-line @typescript-eslint/no-dynamic-delete
792 delete template[deprecatedKey as keyof ChargingStationTemplate]
fba11dc6 793 }
66a7748d 794}
fba11dc6 795
947f048a 796interface ChargingProfilesLimit {
66a7748d
JB
797 limit: number
798 chargingProfile: ChargingProfile
947f048a
JB
799}
800
fba11dc6 801/**
21ee4dc2 802 * Charging profiles shall already be sorted by connector id descending then stack level descending
fba11dc6 803 *
d467756c
JB
804 * @param chargingStation -
805 * @param connectorId -
fba11dc6
JB
806 * @param chargingProfiles -
807 * @param logPrefix -
947f048a 808 * @returns ChargingProfilesLimit
fba11dc6
JB
809 */
810const getLimitFromChargingProfiles = (
a71d4e70
JB
811 chargingStation: ChargingStation,
812 connectorId: number,
fba11dc6 813 chargingProfiles: ChargingProfile[],
66a7748d 814 logPrefix: string
947f048a 815): ChargingProfilesLimit | undefined => {
66a7748d
JB
816 const debugLogMsg = `${logPrefix} ${moduleName}.getLimitFromChargingProfiles: Matching charging profile found for power limitation: %j`
817 const currentDate = new Date()
f938317f 818 const connectorStatus = chargingStation.getConnectorStatus(connectorId)
fba11dc6 819 for (const chargingProfile of chargingProfiles) {
66a7748d 820 const chargingSchedule = chargingProfile.chargingSchedule
2466918c 821 if (chargingSchedule.startSchedule == null) {
109c677a 822 logger.debug(
66a7748d
JB
823 `${logPrefix} ${moduleName}.getLimitFromChargingProfiles: Charging profile id ${chargingProfile.chargingProfileId} has no startSchedule defined. Trying to set it to the connector current transaction start date`
824 )
a71d4e70 825 // OCPP specifies that if startSchedule is not defined, it should be relative to start of the connector transaction
f938317f 826 chargingSchedule.startSchedule = connectorStatus?.transactionStart
52952bf8 827 }
2466918c 828 if (!isDate(chargingSchedule.startSchedule)) {
ef9e3b33 829 logger.warn(
66a7748d
JB
830 `${logPrefix} ${moduleName}.getLimitFromChargingProfiles: Charging profile id ${chargingProfile.chargingProfileId} startSchedule property is not a Date instance. Trying to convert it to a Date instance`
831 )
832 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
5199f9fd 833 chargingSchedule.startSchedule = convertToDate(chargingSchedule.startSchedule)!
ef9e3b33 834 }
2466918c 835 if (chargingSchedule.duration == null) {
da332e70 836 logger.debug(
66a7748d
JB
837 `${logPrefix} ${moduleName}.getLimitFromChargingProfiles: Charging profile id ${chargingProfile.chargingProfileId} has no duration defined and will be set to the maximum time allowed`
838 )
da332e70 839 // OCPP specifies that if duration is not defined, it should be infinite
be9f397b 840 chargingSchedule.duration = differenceInSeconds(maxTime, chargingSchedule.startSchedule)
da332e70 841 }
0eb666db 842 if (!prepareChargingProfileKind(connectorStatus, chargingProfile, currentDate, logPrefix)) {
66a7748d 843 continue
ec4a242a 844 }
0bd926c1 845 if (!canProceedChargingProfile(chargingProfile, currentDate, logPrefix)) {
66a7748d 846 continue
142a66c9 847 }
fba11dc6
JB
848 // Check if the charging profile is active
849 if (
975e18ec 850 isWithinInterval(currentDate, {
2466918c
JB
851 start: chargingSchedule.startSchedule,
852 end: addSeconds(chargingSchedule.startSchedule, chargingSchedule.duration)
975e18ec 853 })
fba11dc6 854 ) {
252a7d22 855 if (isNotEmptyArray(chargingSchedule.chargingSchedulePeriod)) {
80c58041
JB
856 const chargingSchedulePeriodCompareFn = (
857 a: ChargingSchedulePeriod,
66a7748d
JB
858 b: ChargingSchedulePeriod
859 ): number => a.startPeriod - b.startPeriod
80c58041 860 if (
6fc0c6f3 861 !isArraySorted<ChargingSchedulePeriod>(
80c58041 862 chargingSchedule.chargingSchedulePeriod,
66a7748d 863 chargingSchedulePeriodCompareFn
6fc0c6f3 864 )
80c58041
JB
865 ) {
866 logger.warn(
66a7748d
JB
867 `${logPrefix} ${moduleName}.getLimitFromChargingProfiles: Charging profile id ${chargingProfile.chargingProfileId} schedule periods are not sorted by start period`
868 )
869 chargingSchedule.chargingSchedulePeriod.sort(chargingSchedulePeriodCompareFn)
80c58041 870 }
da332e70 871 // Check if the first schedule period startPeriod property is equal to 0
55f2ab60
JB
872 if (chargingSchedule.chargingSchedulePeriod[0].startPeriod !== 0) {
873 logger.error(
66a7748d
JB
874 `${logPrefix} ${moduleName}.getLimitFromChargingProfiles: Charging profile id ${chargingProfile.chargingProfileId} first schedule period start period ${chargingSchedule.chargingSchedulePeriod[0].startPeriod} is not equal to 0`
875 )
876 continue
55f2ab60 877 }
991fb26b 878 // Handle only one schedule period
975e18ec 879 if (chargingSchedule.chargingSchedulePeriod.length === 1) {
252a7d22
JB
880 const result: ChargingProfilesLimit = {
881 limit: chargingSchedule.chargingSchedulePeriod[0].limit,
66a7748d
JB
882 chargingProfile
883 }
884 logger.debug(debugLogMsg, result)
885 return result
41189456 886 }
66a7748d 887 let previousChargingSchedulePeriod: ChargingSchedulePeriod | undefined
252a7d22 888 // Search for the right schedule period
e3037969
JB
889 for (const [
890 index,
66a7748d 891 chargingSchedulePeriod
e3037969 892 ] of chargingSchedule.chargingSchedulePeriod.entries()) {
252a7d22
JB
893 // Find the right schedule period
894 if (
895 isAfter(
2466918c 896 addSeconds(chargingSchedule.startSchedule, chargingSchedulePeriod.startPeriod),
66a7748d 897 currentDate
252a7d22
JB
898 )
899 ) {
e3037969 900 // Found the schedule period: previous is the correct one
252a7d22 901 const result: ChargingProfilesLimit = {
66a7748d 902 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
e3037969 903 limit: previousChargingSchedulePeriod!.limit,
66a7748d
JB
904 chargingProfile
905 }
906 logger.debug(debugLogMsg, result)
907 return result
252a7d22 908 }
e3037969 909 // Keep a reference to previous one
66a7748d 910 previousChargingSchedulePeriod = chargingSchedulePeriod
975e18ec 911 // Handle the last schedule period within the charging profile duration
252a7d22 912 if (
975e18ec
JB
913 index === chargingSchedule.chargingSchedulePeriod.length - 1 ||
914 (index < chargingSchedule.chargingSchedulePeriod.length - 1 &&
ccfa30bc
JB
915 differenceInSeconds(
916 addSeconds(
2466918c 917 chargingSchedule.startSchedule,
66a7748d 918 chargingSchedule.chargingSchedulePeriod[index + 1].startPeriod
ccfa30bc 919 ),
2466918c
JB
920 chargingSchedule.startSchedule
921 ) > chargingSchedule.duration)
252a7d22
JB
922 ) {
923 const result: ChargingProfilesLimit = {
e3037969 924 limit: previousChargingSchedulePeriod.limit,
66a7748d
JB
925 chargingProfile
926 }
927 logger.debug(debugLogMsg, result)
928 return result
252a7d22 929 }
17ac262c
JB
930 }
931 }
932 }
17ac262c 933 }
66a7748d 934}
17ac262c 935
0eb666db 936export const prepareChargingProfileKind = (
f938317f 937 connectorStatus: ConnectorStatus | undefined,
0eb666db 938 chargingProfile: ChargingProfile,
6dde6c5f 939 currentDate: string | number | Date,
66a7748d 940 logPrefix: string
0eb666db
JB
941): boolean => {
942 switch (chargingProfile.chargingProfileKind) {
943 case ChargingProfileKindType.RECURRING:
944 if (!canProceedRecurringChargingProfile(chargingProfile, logPrefix)) {
66a7748d 945 return false
0eb666db 946 }
66a7748d
JB
947 prepareRecurringChargingProfile(chargingProfile, currentDate, logPrefix)
948 break
0eb666db 949 case ChargingProfileKindType.RELATIVE:
be9f397b 950 if (chargingProfile.chargingSchedule.startSchedule != null) {
ccfa30bc 951 logger.warn(
66a7748d
JB
952 `${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`
953 )
954 delete chargingProfile.chargingSchedule.startSchedule
ccfa30bc 955 }
f938317f 956 if (connectorStatus?.transactionStarted === true) {
5199f9fd 957 chargingProfile.chargingSchedule.startSchedule = connectorStatus.transactionStart
ef9e3b33
JB
958 }
959 // FIXME: Handle relative charging profile duration
66a7748d 960 break
0eb666db 961 }
66a7748d
JB
962 return true
963}
0eb666db 964
ad490d5f 965export const canProceedChargingProfile = (
0bd926c1 966 chargingProfile: ChargingProfile,
6dde6c5f 967 currentDate: string | number | Date,
66a7748d 968 logPrefix: string
0bd926c1
JB
969): boolean => {
970 if (
5dc7c990
JB
971 (isValidDate(chargingProfile.validFrom) && isBefore(currentDate, chargingProfile.validFrom)) ||
972 (isValidDate(chargingProfile.validTo) && isAfter(currentDate, chargingProfile.validTo))
0bd926c1
JB
973 ) {
974 logger.debug(
975 `${logPrefix} ${moduleName}.canProceedChargingProfile: Charging profile id ${
976 chargingProfile.chargingProfileId
6dde6c5f 977 } is not valid for the current date ${
5dc7c990 978 isDate(currentDate) ? currentDate.toISOString() : currentDate
6dde6c5f 979 }`
66a7748d
JB
980 )
981 return false
0bd926c1 982 }
ef9e3b33 983 if (
be9f397b
JB
984 chargingProfile.chargingSchedule.startSchedule == null ||
985 chargingProfile.chargingSchedule.duration == null
ef9e3b33 986 ) {
0bd926c1 987 logger.error(
66a7748d
JB
988 `${logPrefix} ${moduleName}.canProceedChargingProfile: Charging profile id ${chargingProfile.chargingProfileId} has no startSchedule or duration defined`
989 )
990 return false
ef9e3b33 991 }
5dc7c990 992 if (!isValidDate(chargingProfile.chargingSchedule.startSchedule)) {
ef9e3b33 993 logger.error(
66a7748d
JB
994 `${logPrefix} ${moduleName}.canProceedChargingProfile: Charging profile id ${chargingProfile.chargingProfileId} has an invalid startSchedule date defined`
995 )
996 return false
ef9e3b33 997 }
5199f9fd 998 if (!Number.isSafeInteger(chargingProfile.chargingSchedule.duration)) {
ef9e3b33 999 logger.error(
66a7748d
JB
1000 `${logPrefix} ${moduleName}.canProceedChargingProfile: Charging profile id ${chargingProfile.chargingProfileId} has non integer duration defined`
1001 )
1002 return false
0bd926c1 1003 }
66a7748d
JB
1004 return true
1005}
0bd926c1 1006
0eb666db 1007const canProceedRecurringChargingProfile = (
0bd926c1 1008 chargingProfile: ChargingProfile,
66a7748d 1009 logPrefix: string
0bd926c1
JB
1010): boolean => {
1011 if (
1012 chargingProfile.chargingProfileKind === ChargingProfileKindType.RECURRING &&
be9f397b 1013 chargingProfile.recurrencyKind == null
0bd926c1
JB
1014 ) {
1015 logger.error(
66a7748d
JB
1016 `${logPrefix} ${moduleName}.canProceedRecurringChargingProfile: Recurring charging profile id ${chargingProfile.chargingProfileId} has no recurrencyKind defined`
1017 )
1018 return false
0bd926c1 1019 }
d929adcc
JB
1020 if (
1021 chargingProfile.chargingProfileKind === ChargingProfileKindType.RECURRING &&
be9f397b 1022 chargingProfile.chargingSchedule.startSchedule == null
d929adcc 1023 ) {
ef9e3b33 1024 logger.error(
66a7748d
JB
1025 `${logPrefix} ${moduleName}.canProceedRecurringChargingProfile: Recurring charging profile id ${chargingProfile.chargingProfileId} has no startSchedule defined`
1026 )
1027 return false
ef9e3b33 1028 }
66a7748d
JB
1029 return true
1030}
0bd926c1 1031
522e4b05 1032/**
ec4a242a 1033 * Adjust recurring charging profile startSchedule to the current recurrency time interval if needed
522e4b05
JB
1034 *
1035 * @param chargingProfile -
1036 * @param currentDate -
1037 * @param logPrefix -
1038 */
0eb666db 1039const prepareRecurringChargingProfile = (
76dab5a9 1040 chargingProfile: ChargingProfile,
6dde6c5f 1041 currentDate: string | number | Date,
66a7748d 1042 logPrefix: string
ec4a242a 1043): boolean => {
66a7748d
JB
1044 const chargingSchedule = chargingProfile.chargingSchedule
1045 let recurringIntervalTranslated = false
5dc7c990 1046 let recurringInterval: Interval | undefined
76dab5a9
JB
1047 switch (chargingProfile.recurrencyKind) {
1048 case RecurrencyKindType.DAILY:
522e4b05 1049 recurringInterval = {
66a7748d 1050 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
522e4b05 1051 start: chargingSchedule.startSchedule!,
66a7748d
JB
1052 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
1053 end: addDays(chargingSchedule.startSchedule!, 1)
1054 }
1055 checkRecurringChargingProfileDuration(chargingProfile, recurringInterval, logPrefix)
522e4b05
JB
1056 if (
1057 !isWithinInterval(currentDate, recurringInterval) &&
991fb26b 1058 isBefore(recurringInterval.end, currentDate)
522e4b05
JB
1059 ) {
1060 chargingSchedule.startSchedule = addDays(
991fb26b 1061 recurringInterval.start,
66a7748d
JB
1062 differenceInDays(currentDate, recurringInterval.start)
1063 )
522e4b05
JB
1064 recurringInterval = {
1065 start: chargingSchedule.startSchedule,
66a7748d
JB
1066 end: addDays(chargingSchedule.startSchedule, 1)
1067 }
1068 recurringIntervalTranslated = true
76dab5a9 1069 }
66a7748d 1070 break
76dab5a9 1071 case RecurrencyKindType.WEEKLY:
522e4b05 1072 recurringInterval = {
66a7748d 1073 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
522e4b05 1074 start: chargingSchedule.startSchedule!,
66a7748d
JB
1075 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
1076 end: addWeeks(chargingSchedule.startSchedule!, 1)
1077 }
1078 checkRecurringChargingProfileDuration(chargingProfile, recurringInterval, logPrefix)
522e4b05
JB
1079 if (
1080 !isWithinInterval(currentDate, recurringInterval) &&
991fb26b 1081 isBefore(recurringInterval.end, currentDate)
522e4b05
JB
1082 ) {
1083 chargingSchedule.startSchedule = addWeeks(
991fb26b 1084 recurringInterval.start,
66a7748d
JB
1085 differenceInWeeks(currentDate, recurringInterval.start)
1086 )
522e4b05
JB
1087 recurringInterval = {
1088 start: chargingSchedule.startSchedule,
66a7748d
JB
1089 end: addWeeks(chargingSchedule.startSchedule, 1)
1090 }
1091 recurringIntervalTranslated = true
76dab5a9 1092 }
66a7748d 1093 break
ec4a242a
JB
1094 default:
1095 logger.error(
66a7748d
JB
1096 `${logPrefix} ${moduleName}.prepareRecurringChargingProfile: Recurring ${chargingProfile.recurrencyKind} charging profile id ${chargingProfile.chargingProfileId} is not supported`
1097 )
76dab5a9 1098 }
66a7748d 1099 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
ec4a242a 1100 if (recurringIntervalTranslated && !isWithinInterval(currentDate, recurringInterval!)) {
522e4b05 1101 logger.error(
aa5c5ad4 1102 `${logPrefix} ${moduleName}.prepareRecurringChargingProfile: Recurring ${
522e4b05 1103 chargingProfile.recurrencyKind
991fb26b 1104 } charging profile id ${chargingProfile.chargingProfileId} recurrency time interval [${toDate(
5dc7c990 1105 recurringInterval?.start as Date
991fb26b 1106 ).toISOString()}, ${toDate(
5dc7c990 1107 recurringInterval?.end as Date
6dde6c5f 1108 ).toISOString()}] has not been properly translated to current date ${
5dc7c990 1109 isDate(currentDate) ? currentDate.toISOString() : currentDate
6dde6c5f 1110 } `
66a7748d 1111 )
522e4b05 1112 }
66a7748d
JB
1113 return recurringIntervalTranslated
1114}
76dab5a9 1115
d476bc1b
JB
1116const checkRecurringChargingProfileDuration = (
1117 chargingProfile: ChargingProfile,
1118 interval: Interval,
66a7748d 1119 logPrefix: string
ec4a242a 1120): void => {
be9f397b 1121 if (chargingProfile.chargingSchedule.duration == null) {
142a66c9
JB
1122 logger.warn(
1123 `${logPrefix} ${moduleName}.checkRecurringChargingProfileDuration: Recurring ${
1124 chargingProfile.chargingProfileKind
1125 } charging profile id ${
1126 chargingProfile.chargingProfileId
1127 } duration is not defined, set it to the recurrency time interval duration ${differenceInSeconds(
1128 interval.end,
66a7748d
JB
1129 interval.start
1130 )}`
1131 )
1132 chargingProfile.chargingSchedule.duration = differenceInSeconds(interval.end, interval.start)
142a66c9 1133 } else if (
be9f397b 1134 chargingProfile.chargingSchedule.duration > differenceInSeconds(interval.end, interval.start)
d476bc1b
JB
1135 ) {
1136 logger.warn(
aa5c5ad4 1137 `${logPrefix} ${moduleName}.checkRecurringChargingProfileDuration: Recurring ${
d476bc1b
JB
1138 chargingProfile.chargingProfileKind
1139 } charging profile id ${chargingProfile.chargingProfileId} duration ${
1140 chargingProfile.chargingSchedule.duration
710d50eb 1141 } is greater than the recurrency time interval duration ${differenceInSeconds(
d476bc1b 1142 interval.end,
66a7748d
JB
1143 interval.start
1144 )}`
1145 )
1146 chargingProfile.chargingSchedule.duration = differenceInSeconds(interval.end, interval.start)
d476bc1b 1147 }
66a7748d 1148}
d476bc1b 1149
fba11dc6 1150const getRandomSerialNumberSuffix = (params?: {
66a7748d
JB
1151 randomBytesLength?: number
1152 upperCase?: boolean
fba11dc6 1153}): string => {
66a7748d
JB
1154 const randomSerialNumberSuffix = randomBytes(params?.randomBytesLength ?? 16).toString('hex')
1155 if (params?.upperCase === true) {
1156 return randomSerialNumberSuffix.toUpperCase()
17ac262c 1157 }
66a7748d
JB
1158 return randomSerialNumberSuffix
1159}