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