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